Base solution for your next web application

Activities of "cyklussoftware"

Hello,

I am in the process of upgrading from ASP.NET Zero v7.1.0 with .NET Core 2.2 and Angular to ASP.NET Zero v8.1.0 with .NET Core 3.1 with Angular. The upgrade is going well and I am in the process of updating my queries to be compatible with EF Core 3.

I just came across some strange behavior with the Current Unit of Work and the NotificationPublisher. I'll show example code below:

Announcement is an AuditedEntity.

I have this line in my AnnouncementsAppService.Create(...) method:

await _announcementRepository.InsertAsync(announcement);

Later in the Create method, I have this line which calls a separate DomainService:

await _announcementDomainService.SendToAppropriateUsers(announcement, matchingAliases);

SendToAppropriateUsers is a public async Task. In this method, I run await _notificationPublisher.PublishAsync(......) with some parameters. Notifications are published successfully and I see a web notification. When I look at my dbo.Announcements table in the database, the Announcement is saved, but the CreatorUserId field is NULL. The CreationTime column is correctly set.

I figured out that if I call await CurrentUnitOfWork.SaveChangesAsync(); anywhere before I call await _notificationPublisher.PublishAsync(......), the CreatorUserId is saved correctly (it is not null). I can also remove await _notificationPublisher.PublishAsync(......) and it will save correctly. I tried making my method virtual with the [UnitOfWork] attribute. This resulted in the same behavior as I've described.

This line in the ASP.NET Boilerplate NotificationPublisher.cs file (see line here) saves the Unit of Work the same way I tried, but the Announcement.CreatorUserId column is still NULL.

Do you know why this is only happening when I run await _notificationPublisher.PublishAsync(......) before I've saved the CurrentUnitOfWork?

Thanks!

I am upgrading from ASP.NET Zero v7.1 (.NET Core 2.2 + Angular) to ASP.NET Zero v8.1 (.NET Core 3.1 + Angular) and I noticed that ALL of the migrations (including the Initial Migration) have changed.

I believe the only change to the previous migrations was that each and every column in the Designer.cs files now have .HasColumnType applied to them. Is this considered a major breaking change for upgrading from previous versions of ASP.NET Zero or from EF Core 2.2 to EF Core 3.1?

Also, for general reference, after generating a migration using the RAD tool, I would manually change the Delete Behavior from CASCADE to RESTRICT. But I never updated the Designer files. Is it necessary to update the designer files or somehow re-create them? Or is it OK to simply update the main Up and Down methods?

Thanks

Hello,

I am using ASP.NET Zero v7.1 with .NET Core 2.2 and Angular.

In my application, we have many Organization entities. Organizations needs their own permission/authorization system so that members of the organization can be given different roles and abilities within the Organization. We refer to the default ASP.NET Zero permission system as the "Global Permission" system and the Organization based permission system as the "Organization Permission" system.

Our Organization Permission system is not meant to be as comprehensive as the Global Permission system. We simply have 4 Permission Levels with a predefined set of abilities. For example:

  • The Level 3 Permission Level can add Members and modify Organization details like it's logo and description.
  • The Level 2 Permission Level can create/update/delete Announcements or other Organization specific entities unrelated to Member management
  • The Level 1 Permission Level has no special permissions, but can see Organization specific entities assigned to them (or other private Organization information)
  • The "Public" Permission Level doesn't actually exist. When a user is in the "Public" Permission Level, it means that a user doesn't belong to the Orgnaization and can only see Organization specific entities marked as "IsPublic"

Orgnanization specific entities like Announcement can be marked as IsPublic OR be assigned to 1 or more Permission Levels.

Now, when a User wants to see a list of Announcements for an Organization, we query the database for all Announcements related to Organization 1 and check 4 things in the WHERE condition of our query:

  • Does the User have any Global Permissions related to Announcements? If yes, then he can view ALL Announcements
  • Is the Announcement Public? If yes, then he can view the Announcement
  • Does the User have a Level 2 or higher Permission Level for the Organization that this Announcement belongs to? If yes, then he can view the Announcement.
  • Is the Announcement assigned to his Permission Level in the Organization? If yes, then he can view the Announcement.

Checking the 4 conditions in the WHERE of our query works, but it requires us to query for a list of the User's Memberships before doing the Announcement query so that the Announcement query can check the User's permission for each Announcement without doing extra JOIN / GROUP BY logic. I am working to make this easier and more efficient by using a TypedCache and a helper class.

When it comes to creating/updating/deleting Announcements, a User must have Global Permission OR Level 2 or higher for the Organization. Right now, this is done doing another query inside of each Create/Update/Delete method (I already have a helper class doing it), but I would like to abstract the permission checking logic into an Attribute or Filter, if possible, so that I don't have to write as much code each time I create a new create/edit/delete endpoint.

Hopefully what I have described makes sense to some degree. Based on what I've described, do you think it's worth trying to go down the custom Authorization Filter / Authorization Policy route for create/update/delete? Or do you think it makes sense to keep querying like I am because it is a non-standard Authorization scheme?

Thanks!

I am using ASP.NET Zero v7.1 with ASP.NET Core and Angular.

I am looking into adding a Unique Constraint to a custom column I added the AbpUsers table. It is a string column with nvarchar(256), like Username. While I was looking through the table to see how the Username column's uniqueness is enforced, I realized that it isn't enforced directly in the database. I am able to insert duplicate rows if I insert directly using SQL. If I try to create a duplicate email or username when registering through the application, I get the error that says it is a duplicate.

This leads me to believe that uniqueness is checked somewhere in the UserManager when creating the user. Is this a correct assumption? Is there a specific reason that unique contraints aren't enforced in this table (and maybe others too)?

I would love to know the reasoning behind this decision so that I can learn from it. Is it because it is a multi-tenant application? Maybe because the AbpUsers table uses soft-delete? I've been adding unique indexes to my custom database tables so that duplicates are strictly forbidden. With the unique indexes I can't accidentally create duplicate data if I introduce a bug in my code. My tables don't have soft-delete so I haven't run into issues with that. And I haven't run into issues with uniqueness across tenants because my unique indexes refer to Entity row Ids (rows in my many-to-many relationship tables needed unique constraints, for example). I suppose that the unique constraint could cause problems if I try to scale a single tenant across multiple databases, but I don't know if that is possible with ASP.NET Zero.

Is there a reason why I shouldn't be creating unique indexes in a multi-tenant application?

I'm looking forward to hearing your thoughts!

I've seen some other posts asking about how to change the branding for individual Tenants. I'm not sure if this is the best place to post this, but over the past 2 days I have been looking into and working on changing themes for ASP.NET Core + Angular v6.5. I might have a solution that will work for some people.

I created a tool that programatically edits the Default theme's CSS files by comparing two different included themes (Default and Pink, for example) and determining which colors are shared and which colors are unique between the two. The CSS that is output works based on CSS variables (https://www.w3schools.com/css/css3_variables.asp). This means that there is a only a single place that colors are defined. With this system, Tenants can upload a CSS document that contains updated variable definitions to reflect their primary branding color. These updated variables overwrite the original variables that the tool outputs, so the web page uses the Tenant colors instead of whatever default color you use.

The tool will generate the entire theme (similar to the Metronic Theme Changer here https://github.com/aspnetzero/metronic, but it doesn't require any new scss to be compiled via Gulp. The outputed Default theme contains the variable references needed to implement branding for individual Tenants. As mentioned, Tenants can change their colors if they upload a CSS document with overriding variable definitions.

I think this is the solution I am going to use. As far as I know, the "official" method might require creating a new theme for every Tenant and baking it directly into the application by default. I could be wrong and there might be a better way to do things. Either way, this was an interesting project to work on for a few days.

You can find the source code (C#) here: https://github.com/Connor14/CSSThemeHelper. Feel free to contribute where you see fit. The tool doesn't minify the css and it doesn't output a dedicated tenant CSS document without overwriting the output files that you specify. It also doesn't consolidate the large number of colors that there are. I have found 62 colors, but I think I forgot to include a CSS file from somewhere. The Metronic Theme Changer project provided by ASP.NET Zero has 69 colors listed and the files that I am pulling from only contained 62.

I hope this helps someone!

I am using version ASP.NET Core + Angular v6.5.0.

I basically copied the image uploading logic from the Profile Picture file upload component and I am having an issue when my custom modal is closing. My code is attached below.

When my close() function runs the uploader.clearQueue(); line, an exception that says TypeError: Cannot read property 'isUploading' of undefined gets thrown. This is part of the file-uploader.class.js file. I think the function that is throwing the exception looks like this:

FileUploader.prototype.removeFromQueue = function (value) {
        var index = this.getIndexOfItem(value);
        var item = this.queue[index];
        if (item.isUploading) {
            item.cancel();
        }
        this.queue.splice(index, 1);
        this.progress = this._getTotalProgress();
};

What's odd is that when I debug this in VS Code, the item variable is defined and isUploading is also defined.

My image upload works and I can see the images reflected on my server, but after running the this.uploader.uploadAll(); function, the queue is not empty. If I check the queue after uploading in the Profile Picture section, the queue is empty. I'm not sure what could be going wrong given that my code is practically a copy. Maybe I'm missing an import somewhere that doesn't throw an error when it's missing?

My component code is below.

import { IAjaxResponse } from '@abp/abpHttpInterceptor';
import { TokenService } from '@abp/auth/token.service';
import { Component, Injector, ViewChild } from '@angular/core';
import { AppConsts } from '@shared/AppConsts';
import { AppComponentBase } from '@shared/common/app-component-base';
import { FileUploader, FileUploaderOptions, FileItem } from 'ng2-file-upload';
import { ModalDirective } from 'ngx-bootstrap';
import { finalize } from 'rxjs/operators';

@Component({
    selector: 'changeRewardImageModal',
    templateUrl: './change-reward-image-modal.component.html'
})
export class ChangeRewardImageModalComponent extends AppComponentBase {

    @ViewChild('changeRewardImageModal') modal: ModalDirective;

    public active = false;
    public uploader: FileUploader;
    public temporaryPictureUrl: string;
    public saving = false;

    private maxProfilPictureBytesUserFriendlyValue = 5;
    private temporaryPictureFileName: string;
    private _uploaderOptions: FileUploaderOptions = {};

    imageChangedEvent: any = '';

    constructor(
        injector: Injector,
        private _tokenService: TokenService
    ) {
        super(injector);
    }

    initializeModal(): void {
        this.active = true;
        this.temporaryPictureUrl = '';
        this.temporaryPictureFileName = '';
        this.initFileUploader();
    }

    show(): void {
        this.initializeModal();
        this.modal.show();
    }

    close(): void {
        console.log("close");
        this.active = false;
        this.imageChangedEvent = '';

        // Clearing the queue gets rid of the files that need to be uploaded.
        // Idk if we actually need to do this if the modal is closing.
        //console.log(this.uploader.queue.length);
        //if(this.uploader.queue.length > 0){
        try {
            this.uploader.clearQueue(); // todo this was causing an exception to be thrown
        } catch (exception){
            console.log(exception);
        }
        //}

        this.modal.hide();
    }

    fileChangeEvent(event: any): void {
        this.imageChangedEvent = event;
    }

    imageCroppedFile(file: File) {
        console.log("cropped file");
        let files: File[] = [file];
        this.uploader.clearQueue();
        this.uploader.addToQueue(files);
    }

    initFileUploader(): void {
        this.uploader = new FileUploader({ url: AppConsts.remoteServiceBaseUrl + '/Reward/UploadRewardImage' });
        this._uploaderOptions.autoUpload = false;
        this._uploaderOptions.authToken = 'Bearer ' + this._tokenService.getToken();
        this._uploaderOptions.removeAfterUpload = true;
        this.uploader.onAfterAddingFile = (file) => {
            file.withCredentials = false;
        };

        this.uploader.onBuildItemForm = (fileItem: FileItem, form: any) => {
            form.append('FileType', fileItem.file.type);
            form.append('FileName', 'RewardImage');
            form.append('FileToken', this.guid());
        };

        this.uploader.onSuccessItem = (item, response, status) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            if (resp.success) {
                this.saving = false;
                abp.event.trigger('rewardImageUploaded', {
                    fileToken: resp.result.fileToken,
                    x: 0,
                    y: 0,
                    width: 0,
                    height: 0
                });
                this.close();
            } else {
                this.message.error(resp.error.message);
            }
        };

        this.uploader.setOptions(this._uploaderOptions);
    }

    guid(): string {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000)
                .toString(16)
                .substring(1);
        }
        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
    }

    save(): void {
        if(this.uploader.queue.length > 0){
            this.saving = true;
            this.uploader.uploadAll();
        }
    }
}
Question

I am running ASP.NET Core + Angular v6.3.1 behind an Nginx proxy.

Similar to this question, https://support.aspnetzero.com/QA/Questions/5665, I want to use {TENANCY_URL}.websitename.com to resolve tenants on the Angular site, but I want to use api.websitename.com to access my API. When I use default.websitename.com and log in, I end up logging into the Host project. Based on your description from the post above, this is intended behavior. Please correct me if I'm wrong.

I added some debugging lines and default is being pulled correctly from the URL. Where is the best place to use the _accountService.isTenantAvailable function to resolve the Tenant ID based on the name?

Please note the following:

In the Angular appconfig.json files I have set the appBaseUrl to https://{TENANCY_NAME}.websitename.com and the removeServiceBaseUrl to https://api.websitename.com.

In the Host appsettings.json files I have set the https://api.websitename.com to be https://api.websitename.com, the ClientRootAddress to https://{TENANCY_NAME}.websitename.com and the CorsOrigins to https://*.websitename.com.

Like I said, I can log into the Host just fine with no errors.

I am running ASP.NET Core + Angular v6.3.1. The Angular and Host projects are running in Docker containers behind an Nginx proxy.

I can log in as the Host just fine with no errors in the Google Chrome console. When I try to change to the Default tenant, I can click "Save", the dialog disappears, and the page refreshes, but the tenant doesn't change. I am still the Host. There are no errors in the Google Chrome console when I try to change tenants.

I get a message saying "There is no tenant defined with name asdf" if I try to log in with an invalid tenant called "asdf".

I also tried switching the project to resolve tenants based on the URL and this always resulted in logging in as the Host as well.

I am able to change tenants when I run the same Docker images in Docker on my local machine WITHOUT an Nginx Proxy, so I believe it is some sort of Nginx Proxy configuration problem.

Here is my site configuration for Angular:

location / {
    proxy_pass http://192.168.1.224:4200;
}

Here is my site configuration for the Host:

location / {
    proxy_pass http://192.168.1.224:9901;
}

location /signalr {
    proxy_pass http://192.168.1.224:9901;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
}

I guess I'd like to know if you know what I am missing.

Thanks

The Web.Host project has the ability to use multiple appsettings.json files based on the ASPNETCORE_ENVIRONMENT environment variable. Would it be possible to have multiple appsettings.json files for the Migrator application like the Web.Host project has? I tried looking into this sort of functionality through separate files and separate entries in the single appsettings.json file, but the Migrator application ONLY uses the Default connection from the appsettings.json file.

If you could point me in the correct direction, that would be great.

I am running ASP.NET Core + jQuery v5.5.0 with .NET Core 2.1. The project was set up with nothing changed except connection strings (I am testing building / deploying to Docker). I am using Docker Toolbox on Windows Server 2008 R2.

My Dockerfile:

FROM microsoft/dotnet:2.1-aspnetcore-runtime

WORKDIR /app
COPY . .

ENTRYPOINT ["dotnet", "MyCompany.MyApp.Web.Mvc.dll"]

My docker-compose.yml

version: "3"
services:
    mvc:
        image: continuoustestingmvc
        environment:
            - ASPNETCORE_ENVIRONMENT=Staging
        deploy:
            replicas: 5
            restart_policy:
                condition: on-failure
        ports:
            - "9903:80"
        volumes:
            - "/c/Users/aspnetcore_logs/:/app/App_Data/Logs"
        networks:
            - webnet
networks:
    webnet:

I open my web browser and I can connect to the website running in the Docker container. The page loads with no errors. When I click "Change tenant", the user friendly "Error! An internal error occurred during your request!" dialog pops up with a loading indicator. It never goes away.

At the same time, the following error appears in the Logs:

Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery token could not be decrypted. ---> System.Security.Cryptography.CryptographicException: The key {f38bd4e9-c084-4734-98bf-f029e03bfa32} was not found in the key ring.

When I run the application on my local machine using ASPNETCORE_ENVIRONMENT=Staging and "dotnet MyCompany.MyApp.Web.Mvc.dll", everything works fine. The problem is only in Docker.

I'm not sure where this problem originates from. Is it an issue with the .NET Core 2.1 Docker Image? Or was it fixed in ASP.NET Zero v5.6?

Showing 1 to 10 of 19 entries