Base solution for your next web application

Activities of "compassinformatics17"

Hi, I checked the web.config possibility also based on reports on StackOverflow. The web.config file generated by Core during deployment is there but it contains no reference to any environment variable. Thanks.

Hi,

Thanks for getting back to me. I understand the log4net config but I'm not sure that is related to my issue. My issue is not so much with configuring logging. The logging works fine. But on our live environment, I want the app to run in production mode. But it is ignoring the ASPNETCORE_ENVIRONMENT variable which is set to Production.

Even though the environment is production, AspNetZero still prints out full stack traces to the screen on error because it is always running in development mode. Any ideas on how to make the framework obey the ASPNETCORE_ENVIRONMENT variable and run in production mode.

Is there perhaps some known issue with the ASPNETCORE_ENVIRONMENT variable and the framework in Azure?

Thanks.

Hi Sorry for the late reply.

I'm using Yarn to handle dependencies as specified in this AspNetZero Tutorial. All works fine but the files are not changed on the azure version.

Below are a few images detailing the config and build task output in DevOps. It runs fine but the JS files are not minified in the artefact once the build completes on DevOps. I cannot see what's amiss and why the files are not being minified since all appears correct to my eyes.

Thanks, Barry.

Hi Guys, I managed to get this sorted. This allows you to access appsettings.json from the context.

// Enable access appsettings.json in the context.
private readonly IConfigurationRoot _appConfiguration = new ConfigurationBuilder()
        .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .Build();

Hi,

OK I did some more investigation on this. It works correctly if I run it locally on my dev machine. But when run in devops, the build process completes without errors but the files are not changed. I am still trying to ascertain the cause, so if anyone else has experienced the same issue, I would be interested in hearing how it was resolved.

No problem, thanks for getting back to me. Note the small function at the end which makes the files editable. This gets around the TFS locking issue for bundle generation.

var gulp = require("gulp");
var path = require('path');
var merge = require("merge-stream");
var globby = require('globby');
var concat = require('gulp-concat');
var less = require('gulp-less');
var uglify = require('gulp-uglify-es').default;
var cleanCss = require('gulp-clean-css');
var postcss = require("gulp-postcss");
var url = require("postcss-url");

var bundleConfig = require(path.resolve(__dirname, 'bundles.json'));
var production = false;

const { watch } = require('gulp');

var styleEntries = {};
var scriptEntries = {};

var viewScripts = globby.sync([
    './wwwroot/view-resources/**/*.js',
    '!./wwwroot/view-resources/**/*.min.js'
]);

var viewStyles = globby.sync([
    './wwwroot/view-resources/**/*.css',
    './wwwroot/view-resources/**/*.less',
    '!./wwwroot/view-resources/**/*.min.css'
]);

var metronicScripts = globby.sync([
    './wwwroot/metronic/**/*.js',
    '!./wwwroot/metronic/**/*.min.js',
    '!./wwwroot/metronic/core/**/*.js'
]);

var metronicStyles = globby.sync([
    './wwwroot/metronic/**/*.css',
    './wwwroot/metronic/**/*.less',
    '!./wwwroot/metronic/**/*.min.css'
]);

function processInputDefinition(input) {
    var result = [];
    for (var i = 0; i < input.length; i++) {
        var url = input[i];
        if (url.startsWith('!')) {
            result.push('!' + path.resolve(__dirname, url.substring(1)));
        } else {
            result.push(path.resolve(__dirname, url));
        }
    }

    return result;
}

function fillScriptBundles() {
    // User defined bundles
    for (var k = 0; k < bundleConfig.scripts.length; k++) {
        var scriptBundle = bundleConfig.scripts[k];
        scriptEntries[scriptBundle.output] = globby.sync(processInputDefinition(scriptBundle.input), { noext: true });
    }

    // View scripts
    for (var i = 0; i < viewScripts.length; i++) {
        var viewScriptName = viewScripts[i].replace('./wwwroot/', '');
        scriptEntries[viewScriptName.replace('.js', '.min.js')] = [path.resolve(__dirname, viewScripts[i])];
    }

    // Metronic scripts
    for (var j = 0; j < metronicScripts.length; j++) {
        var metronicScriptName = metronicScripts[j].replace('./wwwroot/', '');
        scriptEntries[metronicScriptName.replace('.js', '.min.js')] = [path.resolve(__dirname, metronicScripts[j])];
    }
}

function fillStyleBundles() {
    // User defined styles
    for (var k = 0; k < bundleConfig.styles.length; k++) {
        var styleBundle = bundleConfig.styles[k];
        styleEntries[styleBundle.output] = globby.sync(processInputDefinition(styleBundle.input), { noext: true });
    }

    // View styles
    for (var j = 0; j < viewStyles.length; j++) {
        var viewStyleName = viewStyles[j].replace('./wwwroot/', '');

        if (viewStyleName.indexOf('.css') >= 0) {
            styleEntries[viewStyleName.replace('.css', '.min.css')] = [path.resolve(__dirname, 'wwwroot/' + viewStyleName)];
        }

        if (viewStyleName.indexOf('.less') >= 0) {
            styleEntries[viewStyleName.replace('.less', '.min.css')] = [path.resolve(__dirname, 'wwwroot/' + viewStyleName)];
        }
    }

    // Metronic styles
    for (var i = 0; i < metronicStyles.length; i++) {
        var metronicStyleName = metronicStyles[i].replace('./wwwroot/', '');

        if (metronicStyleName.indexOf('.css') >= 0) {
            styleEntries[metronicStyleName.replace('.css', '.min.css')] =
                [path.resolve(__dirname, 'wwwroot/' + metronicStyleName)];
        }

        if (metronicStyleName.indexOf('.less') >= 0) {
            styleEntries[metronicStyleName.replace('.less', '.min.css')] = [path.resolve(__dirname, 'wwwroot/' + metronicStyleName)];
        }
    }
}

function getFileNameFromPath(fullPath) {
    return path.basename(fullPath);
}

function getPathWithoutFileNameFromPath(fullPath) {
    return path.dirname(fullPath);
}

function fillScriptMappings() {
    for (var k = 0; k < bundleConfig.scriptMappings.length; k++) {
        var scriptBundle = bundleConfig.scriptMappings[k];
        var inputFilesToBeCopied = globby.sync(processInputDefinition(scriptBundle.input), { noext: true });
        for (var j = 0; j < inputFilesToBeCopied.length; j++) {
            var outputFileName = path.join(scriptBundle.outputFolder, getFileNameFromPath(inputFilesToBeCopied[j]));
            scriptEntries[outputFileName] = [inputFilesToBeCopied[j]];
        }
    }
}

function createScriptBundles() {
    var tasks = [];
    for (var script in scriptEntries) {
        tasks.push(
            createScriptBundle(script)
        );
    }

    return tasks;
}

function createScriptBundle(script) {
    var bundleName = getFileNameFromPath(script);
    var bundlePath = getPathWithoutFileNameFromPath(script);

    var stream = gulp.src(scriptEntries[script]);

    if (production) {
        stream = stream
            .pipe(uglify());
    }

    return stream.pipe(concat(bundleName))
        .pipe(gulp.dest('wwwroot/' + bundlePath));
}

function createStyleBundles() {
    var tasks = [];
    for (var style in styleEntries) {
        tasks.push(
            createStyleBundle(style)
        );
    }

    return tasks;
}

function createStyleBundle(style) {

    var bundleName = getFileNameFromPath(style);
    var bundlePath = getPathWithoutFileNameFromPath(style);

    var options = {
        url: function (asset) {
            // Ignore absolute URLs
            if (asset.url.substring(0, 1) === '/') {
                return asset.url;
            }

            var outputFolder = '';

            if (asset.url.match(/\.(png|svg|jpg|gif)$/)) {
                outputFolder = 'dist/img';
            } else if (asset.url.match(/\.(woff|woff2|eot|ttf|otf)[?]{0,1}.*$/)) {
                outputFolder = 'dist/fonts';
            } else {
                // Ignore not recognized assets like data:image etc...
                return asset.url;
            }

            var fileName = path.basename(asset.absolutePath);
            var outputPath = path.join(__dirname, '/wwwroot/' + outputFolder + '/');

            gulp.src(asset.absolutePath).pipe(gulp.dest(outputPath));

            return '/' + outputFolder + '/' + fileName;
        }
    };

    var stream = gulp.src(styleEntries[style])
        .pipe(postcss([url(options)]))
        .pipe(less({ math: 'parens-division' }));

    if (production) {
        stream = stream.pipe(cleanCss());
    }

    return stream
        .pipe(concat(bundleName))
        .pipe(gulp.dest('wwwroot/' + bundlePath));
}

function findMatchingElements(path, array) {
    var result = [];
    for (var item in array) {
        if (array[item].indexOf(path) >= 0) {
            result[item] = array[item];
        }
    }

    return result;
}

function watchScriptEntries() {
    for (var script in scriptEntries) {
        var watcher = watch(scriptEntries[script]);

        watcher.on('change', function (path, stats) {
            console.log(`${path} updated`);

            var changedBundles = findMatchingElements(path, scriptEntries);

            for (var changedBundle in changedBundles) {
                createScriptBundle(changedBundle);
            }

        });
    }
}

function watchStyleEntries() {
    for (var style in styleEntries) {
        var watcher = watch(styleEntries[style]);

        watcher.on('change', function (path, stats) {
            console.log(`${path} updated`);

            var changedBundles = findMatchingElements(path, styleEntries);

            for (var changedBundle in changedBundles) {
                createStyleBundle(changedBundle);
            }

        });
    }
}

function build() {

    production = true;

    fillScriptBundles();
    fillStyleBundles();
    fillScriptMappings();

    var scriptTasks = createScriptBundles();
    var styleTasks = createStyleBundles();

    return merge(scriptTasks.concat(styleTasks));
}

function buildDev() {

    fillScriptBundles();
    fillStyleBundles();
    fillScriptMappings();

    var scriptTasks = createScriptBundles();
    var styleTasks = createStyleBundles();

    watchScriptEntries();
    watchStyleEntries();

    console.log('Bundles are being created, please wait...');

    return merge(scriptTasks.concat(styleTasks));
}

function clearReadOnlyAttribute(path) {
    console.log('Ensuring files are editable, please wait...');
    require('child_process').exec('attrib -r "' + path + '\*.*" /s /d');
}

// boneill: Make all files and folders in wwwroot editable. Fixes TFS locking issue for bundle generation.
clearReadOnlyAttribute('./wwwroot/');

exports.build = build;
exports.buildDev = buildDev;

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

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

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.

Showing 1 to 10 of 12 entries