Base solution for your next web application

Activities of "compassinformatics17"

Hi,

Thanks for coming back to me. I had also tried to use the local unit of work in the same manner as specified in #5997

    using (var uow = unitOfWorkManager.Begin(TransactionScopeOption.RequiresNew)) // Add this scope
    {
        ...code goes here
        await unitOfWorkManager.Current.SaveChangesAsync();
        await uow.CompleteAsync();
    }

But when I do it this way, I then get errors that related properties cannot be created in CreateOrUpdateEnquiry(), the specific error was "The instance of {entity type} cannot be tracked because another instance of this type with the same key is already being tracked".

If you specify the unit of work locally in this manner, I would expect that local unit of work to then also be used by functions called from within the using block. Is that correct?

I also tried a local transaction scope but I then received an error that the "Connection currently has transaction enlisted. Finish current transaction and retry".

Note AutoDetectChangesEnabled is false in my function also.

Thanks.

Unfortuntely yes. If I do not implement RejectChanges(), my initial Catch does indeed catch the issue and set the error message but it never gets returned because the auto-executed SaveChanges() then executes and fails with a 500 error.

I refined the RejectChanges() function to be a bit more optimised also.

// Undo any changes in the db context.
        private void RejectContextChanges()
        {
            var changedEntries = _ctx.ChangeTracker.Entries()
                                                                  .Where(e => e.Entity != null
                                                                              && e.State != EntityState.Unchanged 
                                                                              && e.State != EntityState.Detached)
                                                                  .ToList();
            foreach (var entry in changedEntries)
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.State = EntityState.Detached;
                        break;
                    case EntityState.Modified:
                        entry.State = EntityState.Unchanged;
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                        entry.State = EntityState.Unchanged;
                        break;
                }
            }
        }

Hi Guys,

I have encountered some unusual behaviour in the framework and I'd really appreciate some advice in working around it.

I have a service which takes in a csv file, converts to entities and then saves to the db.

For negative testing I was doing the following:

  1. Purposely import a decimal which is too long for the db field.
  2. Catch the error, and return a nicely formatted error to the user.
  3. But the error never returns, instead a 500 Internal error is returned to the user.

I understand from #5997 that this is likely caused by SaveChanges() auto executing upon existing the function. I am catching the internal error but another is then thrown by the auto-execution.

I have tried to use transaction scopes and also a local unit of work but both fail because the operations performed in CreateOrUpdateEnquiry() cannot attach to the local transaction scope. Is there something I am missing here? Is there some way for me to use a local unit of work here so that upon exiting its scope the global context does not contain any unsaved changes?

I do have a working solution as seen below which just scans the context and undoes changes RejectChanges(). But is there a more elegant/efficient way to do this?

Thanks, Barry.

private async Task<ImportedEnquiriesReportDto> CreateEnquiries(List<ImportedEnquiryDto> importedEnquiries)
        {
            _enquiryDictionary = await GetEnquiryDictionary();
            _enquiryStageDictionary = await GetEnquiryStageDictionary();
            _enquiryTypeDictionary = await GetEnquiryTypeDictionary();
            _surveyTypeDictionary = await GetSurveyTypeDictionary();

            var report = new ImportedEnquiriesReportDto();
            List<Enquiry> enquiriesToCreate = new List<Enquiry>();

            try
            {
                _ctx.ChangeTracker.AutoDetectChangesEnabled = false; // Get context and disable EF auto-detect changes. HUGE speed increase!!

                foreach (var importedEnquiry in importedEnquiries)
                {
                    if (importedEnquiry.CanBeImported())
                    {
                        try
                        {
                            // New enquiries are created and added to the enquiriesToCreate list.
                            // Existing enquiries are updated in CreateOrUpdateEnquiry() and picked up by the _ctx.ChangeTracker.DetectChanges() call below.
                            // Newly created enquiries will have a detached state at this point and hence can be distinguished from updates, unchanged etc.
                            var enquiry = await CreateOrUpdateEnquiry(importedEnquiry);
                            if (_ctx.Entry(enquiry).State == EntityState.Detached)
                            {
                                enquiriesToCreate.Add(enquiry);
                            }
                        }
                        catch (Exception)
                        {
                            report.InvalidEnquiries.Add(importedEnquiry);
                        }
                    }
                    else
                    {
                        report.InvalidEnquiries.Add(importedEnquiry);
                    }
                }

                // Add new enquiries in bulk. Only call detect changes once after adding all new enquiries.
                if (enquiriesToCreate.Any())
                {
                    await _ctx.Enquiries.AddRangeAsync(enquiriesToCreate);
                    enquiriesToCreate.Clear();
                }

                // Detect any remaining updates etc.
                _ctx.ChangeTracker.DetectChanges();
                report.EnquiriesCreated = _ctx.ChangeTracker.Entries<Enquiry>().Count(e => e.State == EntityState.Added);
                report.EnquiriesUpdated = _ctx.ChangeTracker.Entries<Enquiry>().Count(e => e.State == EntityState.Modified);
                report.EnquiriesUnchanged = _ctx.ChangeTracker.Entries<Enquiry>().Count(e => e.State == EntityState.Unchanged);
                await _ctx.SaveChangesAsync();
            }
            catch (Exception)
            {
                RejectChanges(); // This works but is there a nicer way??
                throw new UserFriendlyException(L("ErrorSavingToDatabase"));
            }
            finally
            {
                // Turn EF auto detect changes back on.
                _ctx.ChangeTracker.AutoDetectChangesEnabled = true;
            }

            return report;
        }
        
private void RejectChanges()
        {
            foreach (var entry in _ctx.ChangeTracker.Entries())
            {
                switch (entry.State)
                {
                    case EntityState.Modified:
                    case EntityState.Deleted:
                        entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                        entry.State = EntityState.Unchanged;
                        break;
                    case EntityState.Added:
                        entry.State = EntityState.Detached;
                        break;
                }
            }
        }

I managed to get this to work, albeit in a bit of a manual way. I could not find any way to reliably abort the request and return a 404 page, so I have handled it with a redirect and it works well.

public class MyProjectDomainTenantResolveContributor : ITenantResolveContributor, ITransientDependency
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IWebMultiTenancyConfiguration _multiTenancyConfiguration;
        private readonly IRepository<Tenant> _tenantRepository;
        private readonly IConfigurationRoot _appConfiguration;

        public MyProjectDomainTenantResolveContributor(
            IHttpContextAccessor httpContextAccessor,
            IWebMultiTenancyConfiguration multiTenancyConfiguration,
            IRepository<Tenant> tenantRepository,
            IAppConfigurationAccessor appConfigurationAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
            _multiTenancyConfiguration = multiTenancyConfiguration;
            _tenantRepository = tenantRepository;
            _appConfiguration = appConfigurationAccessor.Configuration;
        }

        public int? ResolveTenantId()
        {
            if (_multiTenancyConfiguration.DomainFormat.IsNullOrEmpty())
            {
                return null;
            }

            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null)
            {
                return null;
            }

            var hostName = httpContext.Request.Host.Host.RemovePreFix("http://", "https://").RemovePostFix("/");
            var domainFormat = _multiTenancyConfiguration.DomainFormat.RemovePreFix("http://", "https://").Split(':')[0].RemovePostFix("/");
            var result = new FormattedStringValueExtracter().Extract(hostName, domainFormat, true, '/');

            if (!result.IsMatch || !result.Matches.Any())
            {
                // Allow local testing in iis express without redirecting.
                if (hostName == "localhost")
                {
                    return null;
                }

                // No tenant, force our default tenant.
                httpContext.Response.Redirect(_appConfiguration["App:MissingTenantUrl"]);
                return null;
            }

            var tenancyName = result.Matches[0].Value;
            if (tenancyName.IsNullOrEmpty() || string.Equals(tenancyName, "www", StringComparison.OrdinalIgnoreCase))
            {
                // Allow local testing in iis express without redirecting.
                if (hostName == "localhost")
                {
                    return null;
                }

                // No tenant, force our default tenant.
                httpContext.Response.Redirect(_appConfiguration["App:MissingTenantUrl"]);
                return null;
            }

            // Allow the use of admin.domain.com as the host url.
            if (string.Equals(tenancyName, "admin", StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }

            // Check if a tenant with the given name exists.
            var tenant = _tenantRepository.FirstOrDefault(t => t.TenancyName == tenancyName);

            if (tenant == null)
            {
                // No tenant found matching the given name, return 404.
                httpContext.Response.StatusCode = 404;
                httpContext.Response.Redirect(_appConfiguration["App:MissingTenantUrl"] + "/Error?statusCode=404");
                return null;
            }

            // Return tenant id.
            return tenant.Id;
        }
    }

I have written a customised DomainTenantResolveContributor which works just fine. It allows me to access the host via admin.{domain}.com and my various sub-domain tenants. One thing is escaping me however. I would like to return a 404 page when a tenant is not found. This seems like it should be simple, but no matter what I try in the framework, I cannot seem to kill the request and return a 404 if the tenant is not found. I have tried multiple approaches all with the same result, they just result in being sent to the host login which is certainly not desirable.

            var tenantInfo = _tenantStore.Find(tenancyName);
            if (tenantInfo == null)
            {
                // No tenant found matching the given name, return 404.
                throw new EntityNotFoundException();
            }

I'd really appreciate any advice on how to kill the request and redirect to a 404 here. From my reading .NET core should auto handle these types of exceptions but that is not happening here.

Thanks, Barry.

Brilliant thanks and apologies for the late reply, was on other projects.

For anyone trying to do the same see the step by step below:

  1. Create a custom DomainTenantResolver. I have stored this in the root of the Web.Core project.
using System;
using System.Linq;
using Abp.Dependency;
using Abp.Extensions;
using Abp.MultiTenancy;
using Abp.Text;
using Abp.Web.MultiTenancy;
using Microsoft.AspNetCore.Http;

namespace MyProject.Web
{
    public class CustomDomainTenantResolveContributor : ITenantResolveContributor, ITransientDependency
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly IWebMultiTenancyConfiguration _multiTenancyConfiguration;
        private readonly ITenantStore _tenantStore;

        public CustomDomainTenantResolveContributor(
            IHttpContextAccessor httpContextAccessor,
            IWebMultiTenancyConfiguration multiTenancyConfiguration,
            ITenantStore tenantStore)
        {
            _httpContextAccessor = httpContextAccessor;
            _multiTenancyConfiguration = multiTenancyConfiguration;
            _tenantStore = tenantStore;
        }

        public int? ResolveTenantId()
        {
            if (_multiTenancyConfiguration.DomainFormat.IsNullOrEmpty())
            {
                return null;
            }

            var httpContext = _httpContextAccessor.HttpContext;
            if (httpContext == null)
            {
                return null;
            }

            var hostName = httpContext.Request.Host.Host.RemovePreFix("http://", "https://").RemovePostFix("/");
            var domainFormat = _multiTenancyConfiguration.DomainFormat.RemovePreFix("http://", "https://").Split(':')[0].RemovePostFix("/");
            var result = new FormattedStringValueExtracter().Extract(hostName, domainFormat, true, '/');

            if (!result.IsMatch || !result.Matches.Any())
            {
                return null;
            }

            var tenancyName = result.Matches[0].Value;
            if (tenancyName.IsNullOrEmpty())
            {
                return null;
            }

            if (string.Equals(tenancyName, "www", StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }

            // Allow the use of admin.domain.com as the host url.
            if (string.Equals(tenancyName, "admin", StringComparison.OrdinalIgnoreCase))
            {
                return null;
            }

            var tenantInfo = _tenantStore.Find(tenancyName);
            if (tenantInfo == null)
            {
                return null;
            }

            return tenantInfo.Id;
        }
    }
}
  1. Add your custom resolver to MyProjectWebCoreModule.cs in the Web.Core project.

     public override void PreInitialize()
     {
         ...previous code
    
         Configuration.MultiTenancy.Resolvers.Insert(0, typeof(CustomDomainTenantResolveContributor));
    
         ...any additional code
     }
    
  2. Finally set up sub-domain bindings and dns entries in your hosting config.

Thanks, Barry.

Hi Guys, This one seems like it should be easy but I cannot seem to find any reference to it in the docs.

I have enabled sub-domain wildcard multi-tenancy and it works just fine as specifed in the docs.

But when it is enabled, how do I then log in as the host user? If I go to the root domain sitename.com, it just defaults to the default tenant. Is there a way to configure a url for the host such as admin.sitename.com?

Thanks, Barry.

OK great thanks for the assistance. Much appreciated.

Hi, Not specifically a development question but I received a response today to another question which I had asked. I needed to add my GitHub username to our listed GitHub members. I did this and it reported that I had been added successfully. However I did not reveive any email. I also checked current invitations via my GitHub account and I have not received any invitations to accept. Is there currently an issue with the invitations? Thanks, Barry.

Hi,

I am testing minification in ASPNetZero Core. I can run npm run create-bundles without issue but when I run npm run build, I get the error below.

The error seems to be with /view-resources/Areas/App/Views/Common/_KeyValueListManager.js. On line 113 it appears to not be able to handle the period.

[1/4] Resolving packages... success Already up-to-date. Done in 0.61s. [17:19:18] Using gulpfile D:\CompassCoreJqueryDemo\src\CompassCorejQueryDemo.Web.Mvc\gulpfile.js [17:19:18] Starting 'build'... [17:19:33] 'build' errored after 15 s [17:19:33] SyntaxError in plugin "gulp-uglify-es" Message: Unexpected token: punc (.) Details: filename: _KeyValueListManager.js line: 113 col: 36 pos: 4251 domainEmitter: [object Object] domainThrown: false

npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! [email protected] build: yarn && gulp build npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the [email protected] build script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

Thanks, Barry.

Showing 11 to 20 of 23 entries