Yes, implement an IQueryable for your query and then execute with the ToList() or await ToListAsync(). If you would like to post your code I can advise further.
@moetarhini - there is no foreign key because it is a multi-database solution. Your entity may not be in the same database as your tenant.
Hi @moetarhini - your entities may be saved in a different database to your tenants. It would not be a good idea with the Zero framework which explicitly allows a multi-database solution. As a DBA I would also question the use of composite keys, amongst other things they can cause havoc with replication but maybe I'm just old-school.
@timmackey, implement an sql repository in the entity framework project and call the methods directly from your app service. All of my grid information is read in such a way, sometimes it is simpler and more performant to work directly with the database without the Zero overhead.
As promised: The use case here is to send data to the host dashboard from a service in the application layer. The following dashboard widget is interactively updated using SignalR.
In Web.Core project create a new hub, e.g
using Abp.Dependency;
using Abp.Runtime.Session;
using Castle.Core.Logging;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
namespace Nuagecare.Web.App.SignalR.FormSubmissionHub
{
public class FormSubmissionHub : Hub, ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public FormSubmissionHub()
{
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("getFormSubmission", string.Format("User {0}: {1}", AbpSession.UserId, message));
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Logger.Debug("A client connected to FormSubmissionHub: " + Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
Logger.Debug("A client disconnected from FormSubmissionHub: " + Context.ConnectionId);
}
}
}
Create a hub communicator in the same location:
public class SignalRFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator, ITransientDependency
{
private readonly IHubContext<FormSubmissionHub> _formSubmissionHub;
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public SignalRFormSubmissionCommunicator(IHubContext<FormSubmissionHub> formSubmissionHub)
{
_formSubmissionHub = formSubmissionHub;
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendFormSubmission(int tenantId)
{
await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId);
}
}
The interface for the communicator goes in the Core project. Now comes the bit of magic from the Zero guys to give you access to the communicator from the Application project. It’s all a bit wierd and took some fiddling with. In the same location as your communicator interface, in the Core project create a null communicator class:
public class NullFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator
{
public async Task SendFormSubmission(int tenantId)
{
await Task.CompletedTask;
}
}
To complete the plumbing we register our null communicator class and somehow, by magic, your own communicator is used. Go figure. In the PostInitialize() function of your CoreModule add the following line:
IocManager.RegisterIfNot<ISignalRFormSubmissionCommunicator, NullFormSubmissionCommunicator>();
All of this work is so we can call the hub from the application project with a line similar to that below.
await _signalRFormSubmissionCommunicator.SendFormSubmission((int)AbpSession.TenantId);
This message is sent to all connected clients so the next job is to hook it up in angular.
In the angular project create a new component using angular-cli below the admin dashboard and display this on your dashboard. In the same location create a new service using angular-cli called HostDashboardSignalR, something like below. I have created an ngx-tabsheet on the Zero dashboard where I add my own tabbed content, my new tab is called ApiTab. Your service file looks like this:
import { Injectable, Injector, NgZone } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { HubConnection } from '@aspnet/signalr';
@Injectable()
export class FormSubmissionSignalrService extends AppComponentBase {
constructor(
injector: Injector,
public _zone: NgZone
) {
super(injector);
}
formSubmissionHub: HubConnection;
isFormSubmissionHubConnected = false;
init(): void {
this._zone.runOutsideAngular(() => {
abp.signalr.connect();
abp.signalr.startConnection(abp.appPath + 'signalr-formSubmission', connection => {
abp.event.trigger('app.formSubmission.connected');
this.isFormSubmissionHubConnected = true;
this.configureConnection(connection);
});
});
}
configureConnection(connection): void {
// Set the common hub
this.formSubmissionHub = connection;
// Reconnect if hub disconnects
connection.onclose(e => {
this.isFormSubmissionHubConnected = false;
if (e) {
abp.event.trigger('app.formSubmission.disconnected', e);
} else {
abp.event.trigger('app.formSubmission.disconnected');
}
if (!abp.signalr.autoConnect) {
return;
}
setTimeout(() => {
connection.start().then(result => {
console.log(result);
this.isFormSubmissionHubConnected = true;
});
}, 5000);
});
// Register to get notifications
this.registerGetFormSubmission(connection);
}
registerGetFormSubmission(connection): void {
connection.on('getFormSubmission', message => {
abp.event.trigger('app.formSubmission.newSubmission', message);
});
}
}
Make sure you add it as a provider to your admin.module.ts file:
providers: [
ImpersonationService,
TreeDragDropService,
FormSubmissionSignalrService,
Inject this service into your new component and you’re good to go. Here’s my component:
import { Component, OnInit, Injector, NgZone } from '@angular/core';
import { FormSubmissionSignalrService } from '../host-dashboard-signalr/formSubmission-signalr.service';
import { HostDashboardServiceProxy, GetHostDashboardDataOutput, GetTenantSummaryDataForHostDashboard_Output } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/common/app-component-base';
import { SignalRHelper } from '@shared/helpers/SignalRHelper';
import * as moment from 'moment';
import * as _ from 'lodash';
@Component({
selector: 'app-host-dashboard-apitab',
templateUrl: './host-dashboard-apitab.component.html',
styleUrls: ['./host-dashboard-apitab.component.css']
})
export class HostDashboardAPITabComponent extends AppComponentBase implements OnInit {
public connected: string;
public formCount: number;
loading = false;
hostDashboardData = new GetHostDashboardDataOutput();
tenantSummary = new Array<GetTenantSummaryDataForHostDashboard_Output>();
public isFormSubmissionHubConnected: boolean;
constructor(
injector: Injector,
private _formSubmissionSignalrService: FormSubmissionSignalrService,
private _hostDashboardService: HostDashboardServiceProxy,
public _zone: NgZone
) {
super(injector);
}
init(): void {
this.getTenantDataForHostDashboard();
this.registerEvents();
}
ngOnInit(): void {
if (this.appSession.application) {
SignalRHelper.initSignalR(() => { this._formSubmissionSignalrService.init(); });
}
this.init();
}
getTenantDataForHostDashboard() {
let midnight = moment().startOf('day');
let now = moment();
this.loading = true;
this._hostDashboardService.getTenantDataForHostDashboard(
midnight,
now,
0
).subscribe(result => {
this.hostDashboardData = result;
this.tenantSummary = _.orderBy(result.tenantSummary, 'formSubmissionSum', 'desc');
this.loading = false;
});
}
registerEvents(): void {
abp.event.on('app.formSubmission.connected', (message) => {
this._zone.run(() => {
this.isFormSubmissionHubConnected = true;
this.connected = 'hub connected';
});
});
abp.event.on('app.formSubmission.disconnected', (message) => {
this._zone.run(() => {
this.isFormSubmissionHubConnected = false;
this.connected = 'hub disconnected: ' + message;
});
});
abp.event.on('app.formSubmission.newSubmission', (message) => {
this._zone.run(() => {
this.updateTenant(message);
this.formCount = this.formCount + 1;
});
});
}
formatDate(tenant: GetTenantSummaryDataForHostDashboard_Output): string {
if (tenant.formSubmissionSum === 0) {
return '';
} else {
return moment(moment(tenant.lastUpdated).utc().valueOf()).format('DD/MM/YYYY H:mm:ss');
}
}
For good measure here is my html;
<div class="col-lg-4 col-md-6 col-sm-12">
<div class="card border-primary rounded-0 mb-2">
<div class="card-header bg-primary text-white">
<h5><i class="flaticon-profile mr-3"></i>API Stats</h5>
</div>
<div class="card-body">
<div *ngIf="isFormSubmissionHubConnected" class="small text-muted m--font-success"><small>{{connected}}</small></div>
<div *ngIf="!isFormSubmissionHubConnected" class="small text-muted m--font-danger"><small>{{connected}}</small></div>
<div class="row">
<div class="col-4">
<h6>Tenant</h6>
</div>
<!-- <div class="col-4">
<h6>Users</h6>
</div> -->
<div class="col-4">
<h6>Care actions</h6>
</div>
<div class="col-4">
<h6>Last updated</h6>
</div>
</div>
<div class="row" *ngFor="let tenant of tenantSummary; index as i">
<div class="col-4">{{tenant.tenancyName}}</div>
<!-- <div class="col-4">{{tenant.signedInUsersSum}}</div> -->
<div class="col-4">
<strong>{{tenant.formSubmissionSum}}</strong>
</div>
<div class="col-4">
<strong>{{formatDate(tenant)}}</strong>
</div>
</div>
</div>
</div>
</div>
The result is: The numbers are updated from the hub and the last updated is modified on the client. I have a lot of apps entering data into my Zero implementation so this simple table allows me to see that all of my tenants are working at a glance. I thought Musa could use this for the new dashboard, number of active users by tenant would be great.
@ismcagdas, I finally got this working. Give the weekend and i will write up how I did it then you can use it in your documentation should you so desire. Then I'll close the issue.
Taking this a bit further I noticed the line:
IocManager.RegisterIfNot<IChatCommunicator, NullChatCommunicator>();
in [ProjectName]CoreModule.cs in the Core project.
I create a new class, NullFormSubmissionCommunicator in the same folder as my interface in the Core project:
using System.Threading.Tasks;
namespace Nuagecare.App.SignalR
{
public class NullFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator
{
public async Task SendFormSubmission(int tenantId)
{
await Task.CompletedTask;
}
}
}
and add the following to my module:
IocManager.RegisterIfNot<ISignalRFormSubmissionCommunicator, NullFormSubmissionCommunicator>();
But the message is still not received in the angular project. I must be close but can't yet light up my cigar. What am I missing?
Hi @ismcagdas, No problem on the delay, I know 7.2 was a big job and took a lot of your time so I didn't interrupt. This is still a work in progress but as soon as I have SignalR working I will be using it more and more in my dashboards (hence the impatience for the configurable dashboards!). Here is my angular service, formSubmission-signalr.service.ts:
import { Injectable, Injector, NgZone } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { HubConnection } from '@aspnet/signalr';
@Injectable()
export class FormSubmissionSignalrService extends AppComponentBase {
constructor(
injector: Injector,
public _zone: NgZone
) {
super(injector);
}
formSubmissionHub: HubConnection;
isFormSubmissionHubConnected = false;
init(): void {
this._zone.runOutsideAngular(() => {
abp.signalr.connect();
abp.signalr.startConnection(abp.appPath + 'signalr-formSubmission', connection => {
abp.event.trigger('app.formSubmission.connected');
this.isFormSubmissionHubConnected = true;
this.configureConnection(connection);
});
});
}
configureConnection(connection): void {
// Set the common hub
this.formSubmissionHub = connection;
// Reconnect if hub disconnects
connection.onclose(e => {
this.isFormSubmissionHubConnected = false;
if (e) {
abp.log.debug('Form submission hub connection closed with error: ' + e);
} else {
abp.log.debug('Form submission hub disconnected');
}
if (!abp.signalr.autoConnect) {
return;
}
setTimeout(() => {
connection.start().then(result => {
this.isFormSubmissionHubConnected = true;
});
}, 5000);
});
// Register to get notifications
//this.registerChatEvents(connection);
this.registerGetFormSubmission(connection);
}
registerGetFormSubmission(connection): void {
connection.on('getFormSubmission', message => {
abp.event.trigger('app.formSubmission.newSubmission', message);
});
}
sendMessage(messageData, callback): void {
if (!this.isFormSubmissionHubConnected) {
if (callback) {
callback();
}
abp.notify.warn(this.l('ChatIsNotConnectedWarning'));
return;
}
this.formSubmissionHub.invoke('sendMessage', messageData).then(result => {
if (result) {
abp.notify.warn(result);
}
if (callback) {
callback();
}
}).catch(error => {
abp.log.error(error);
if (callback) {
callback();
}
});
}
}
My understanding that once we hit the line:
await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId);
in my server the message should be sent to all clients and then be seen at this point in angular:
registerGetFormSubmission(connection): void {
connection.on('getFormSubmission', message => {
abp.event.trigger('app.formSubmission.newSubmission', message);
});
}
Am I missing something? I must admit that the plumbing for the communicator class in dotnet-core still has me scratching my head as to how the dependencies work.
Hi @ismcagdas, I know your up to your ears with 7.2 but when you get a moment afterwards, could you take a look at this. I have created my Hub (above) in Web.Core. I can call this from the angular project and trigger a send from the same server, everything is good. Following your chat code I have attemped to send a message to all clients as follows. I create a new communicator class in my Web.Core in the same folder (namespace) as my hub:
using Abp.Dependency;
using Abp.Runtime.Session;
using Castle.Core.Logging;
using Microsoft.AspNetCore.SignalR;
using Nuagecare.App.SignalR;
using System.Threading.Tasks;
namespace Nuagecare.Web.App.SignalR.FormSubmissionHub
{
public class SignalRFormSubmissionCommunicator : ISignalRFormSubmissionCommunicator, ITransientDependency
{
private readonly IHubContext<FormSubmissionHub> _formSubmissionHub;
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public SignalRFormSubmissionCommunicator(IHubContext<FormSubmissionHub> formSubmissionHub)
{
_formSubmissionHub = formSubmissionHub;
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendFormSubmission(int tenantId)
{
await _formSubmissionHub.Clients.All.SendAsync("getFormSubmission", tenantId);
}
}
}
I create the interface in my Core project. I inject my communicator into my app service:
private readonly ISignalRFormSubmissionCommunicator _signalRFormSubmissionCommunicator;
and make a simple call:
await _signalRFormSubmissionCommunicator.SendFormSubmission((int)AbpSession.TenantId);
The code hits my communicator method and everything looks fine but the call is not recieved in my angular project. Any simple pointers?
Hi @ismcagdas, Supposing I implemented a controller in the Web.Host project, call my app service to process one document at a time and then called the Hub in Web.Core with my message at the end of each iteration? Architecturally does this break any rules or can you think of any reason why this approach would not work?