Browse Source

Merge pull request #13161 from abpframework/auto-merge/rel-5-3/1167

Merge branch dev with rel-5.3
pull/13165/head
maliming 4 years ago
committed by GitHub
parent
commit
71e637deb6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      docs/en/Multi-Tenancy.md
  2. 16
      docs/zh-Hans/Multi-Tenancy.md
  3. 94
      framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
  4. 10
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs
  5. 2
      npm/ng-packs/packages/core/src/lib/core.module.ts
  6. 8
      npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts
  7. 50
      npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts
  8. 6
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html
  9. 2
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html
  10. 4
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html
  11. 8
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html

16
docs/en/Multi-Tenancy.md

@ -32,6 +32,22 @@ Configure<AbpMultiTenancyOptions>(options =>
> Multi-Tenancy is disabled in the ABP Framework by default. However, it is **enabled by default** when you create a new solution using the [startup template](Startup-Templates/Application.md). `MultiTenancyConsts` class in the solution has a constant to control it in a single place.
### AbpMultiTenancyOptions: Handle inactive and non-existent tenants.
The `MultiTenancyMiddlewareErrorPageBuilder` of `AbpMultiTenancyOptions` is used to handle inactive and non-existent tenants.
It will respond to an error page by default, you can change it if you want, eg: only output the error log and continue ASP NET Core's request pipeline.
```csharp
Configure<AbpMultiTenancyOptions>(options =>
{
options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
{
// Handle the exception.
};
});
```
### Database Architecture
ABP Framework supports all the following approaches to store the tenant data in the database;

16
docs/zh-Hans/Multi-Tenancy.md

@ -34,6 +34,22 @@ namespace MyCompany.MyProject
> 随着"Multi-tenancy ready"的概念,我们打算开发我们的代码和多租户方法兼容.然后它可以被用于多租户和非多租户的程序中,这取决于最终程序的需求.
### AbpMultiTenancyOptions: 处理不活跃或不存在的租户
`MultiTenancyMiddlewareErrorPageBuilder``AbpMultiTenancyOptions` 用于 处理不活跃或不存在的租户.
默认情况下会响应错误页面, 你可以根据自己的需要更改它, 比如: 只输出错误日志并继续ASP NET Core的请求管道
```csharp
Configure<AbpMultiTenancyOptions>(options =>
{
options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
{
// Handle the exception.
};
});
```
#### 定义实体
你可以在你的实体中实现 **IMultiTenant** 接口来实现多租户,例如:

94
framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs

@ -162,13 +162,13 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
protected async override Task PublishToEventBusAsync(Type eventType, object eventData)
{
await PublishAsync(
AbpKafkaEventBusOptions.TopicName,
eventType,
eventData,
new Headers
{
{ "messageId", System.Text.Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N")) }
},
null
{ "messageId", System.Text.Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N")) }
}
);
}
@ -188,42 +188,31 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
new Headers
{
{ "messageId", System.Text.Encoding.UTF8.GetBytes(outgoingEvent.Id.ToString("N")) }
},
null
}
);
}
public override Task PublishManyFromOutboxAsync(IEnumerable<OutgoingEventInfo> outgoingEvents, OutboxConfig outboxConfig)
{
var producer = ProducerPool.Get();
var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName);
var outgoingEventArray = outgoingEvents.ToArray();
producer.BeginTransaction();
try
foreach (var outgoingEvent in outgoingEventArray)
{
foreach (var outgoingEvent in outgoingEventArray)
var messageId = outgoingEvent.Id.ToString("N");
var headers = new Headers
{
var messageId = outgoingEvent.Id.ToString("N");
var headers = new Headers
{
{ "messageId", System.Text.Encoding.UTF8.GetBytes(messageId)}
};
producer.Produce(
AbpKafkaEventBusOptions.TopicName,
new Message<string, byte[]>
{
Key = outgoingEvent.EventName,
Value = outgoingEvent.EventData,
Headers = headers
});
}
{ "messageId", System.Text.Encoding.UTF8.GetBytes(messageId)}
};
producer.CommitTransaction();
}
catch (Exception e)
{
producer.AbortTransaction();
throw;
producer.Produce(
AbpKafkaEventBusOptions.TopicName,
new Message<string, byte[]>
{
Key = outgoingEvent.EventName,
Value = outgoingEvent.EventData,
Headers = headers
});
}
return Task.CompletedTask;
@ -253,47 +242,22 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
return Serializer.Serialize(eventData);
}
public virtual async Task PublishAsync(Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
{
await PublishAsync(
AbpKafkaEventBusOptions.TopicName,
eventType,
eventData,
headers,
headersArguments
);
}
private Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
private Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);
return PublishAsync(topicName, eventName, body, headers, headersArguments);
return PublishAsync(topicName, eventName, body, headers);
}
private Task<DeliveryResult<string, byte[]>> PublishAsync(
string topicName,
string eventName,
byte[] body,
Headers headers,
Dictionary<string, object> headersArguments)
Headers headers)
{
var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName);
return PublishAsync(producer, topicName, eventName, body, headers, headersArguments);
}
private Task<DeliveryResult<string, byte[]>> PublishAsync(
IProducer<string, byte[]> producer,
string topicName,
string eventName,
byte[] body,
Headers headers,
Dictionary<string, object> headersArguments)
{
SetEventMessageHeaders(headers, headersArguments);
return producer.ProduceAsync(
topicName,
new Message<string, byte[]>
@ -304,20 +268,6 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
});
}
private void SetEventMessageHeaders(Headers headers, Dictionary<string, object> headersArguments)
{
if (headersArguments == null)
{
return;
}
foreach (var header in headersArguments)
{
headers.Remove(header.Key);
headers.Add(header.Key, Serializer.Serialize(header.Value));
}
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return HandlerFactories.GetOrAdd(

10
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs

@ -41,16 +41,8 @@ public class ProducerPool : IProducerPool, ISingletonDependency
{
var producerConfig = new ProducerConfig(Options.Connections.GetOrDefault(connection));
Options.ConfigureProducer?.Invoke(producerConfig);
if (producerConfig.TransactionalId.IsNullOrWhiteSpace())
{
producerConfig.TransactionalId = Guid.NewGuid().ToString();
}
var producer = new ProducerBuilder<string, byte[]>(producerConfig).Build();
producer.InitTransactions(DefaultTransactionsWaitDuration);
return new ProducerBuilder<string, byte[]>(producerConfig).Build();
return producer;
})).Value;
}

2
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -136,6 +136,7 @@ export class CoreModule {
return {
ngModule: RootCoreModule,
providers: [
OAuthModule.forRoot().providers,
LocaleProvider,
CookieLanguageProvider,
{
@ -190,7 +191,6 @@ export class CoreModule {
useValue: localizationContributor(options.localizations),
deps: [LocalizationService],
},
OAuthModule.forRoot().providers,
],
};
}

8
npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts

@ -18,6 +18,8 @@ import { PermissionService } from '../services/permission.service';
export class PermissionDirective implements OnDestroy, OnChanges {
@Input('abpPermission') condition: string | undefined;
@Input('abpPermissionRunChangeDetection') runChangeDetection = true;
subscription!: Subscription;
constructor(
@ -38,7 +40,11 @@ export class PermissionDirective implements OnDestroy, OnChanges {
.subscribe(isGranted => {
this.vcRef.clear();
if (isGranted) this.vcRef.createEmbeddedView(this.templateRef);
this.cdRef.detectChanges();
if (this.runChangeDetection) {
this.cdRef.detectChanges();
} else {
this.cdRef.markForCheck();
}
});
}

50
npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts

@ -2,10 +2,12 @@ import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/je
import { Subject } from 'rxjs';
import { PermissionDirective } from '../directives/permission.directive';
import { PermissionService } from '../services';
import { ChangeDetectorRef } from '@angular/core';
describe('PermissionDirective', () => {
let spectator: SpectatorDirective<PermissionDirective>;
let directive: PermissionDirective;
let cdr: ChangeDetectorRef;
const grantedPolicy$ = new Subject<boolean>();
const createDirective = createDirectiveFactory({
directive: PermissionDirective,
@ -30,7 +32,7 @@ describe('PermissionDirective', () => {
grantedPolicy$.next(true);
expect(spectator.query('#test-element')).toBeTruthy();
grantedPolicy$.next(false);
// expect(spectator.query('#test-element')).toBeFalsy(); // TODO: change detection problem should be fixed
expect(spectator.query('#test-element')).toBeFalsy();
});
});
@ -41,6 +43,7 @@ describe('PermissionDirective', () => {
{ hostProps: { condition: '' } },
);
directive = spectator.directive;
cdr = (directive as any).cdRef as ChangeDetectorRef;
});
it('should be created', () => {
@ -55,8 +58,22 @@ describe('PermissionDirective', () => {
grantedPolicy$.next(false);
expect(spectator.query('#test-element')).toBeFalsy();
grantedPolicy$.next(true);
expect(spectator.queryAll('#test-element')).toHaveLength(1);
});
it('should call detect changes method', () => {
const detectChanges = jest.spyOn(cdr, 'detectChanges');
expect(spectator.query('#test-element')).toBeFalsy();
spectator.setHostInput({ condition: 'test' });
grantedPolicy$.next(true);
expect(spectator.query('#test-element')).toBeTruthy();
expect(detectChanges).toHaveBeenCalled();
grantedPolicy$.next(false);
expect(spectator.query('#test-element')).toBeFalsy();
expect(detectChanges).toHaveBeenCalled();
grantedPolicy$.next(true);
expect(spectator.queryAll('#test-element')).toHaveLength(1);
expect(detectChanges).toHaveBeenCalled();
});
describe('#subscription', () => {
@ -70,4 +87,35 @@ describe('PermissionDirective', () => {
});
});
});
describe('with runChangeDetection Input', () => {
beforeEach(() => {
spectator = createDirective(
'<div id="test-element" *abpPermission="condition;runChangeDetection:false">Testing Permission Directive</div>',
{ hostProps: { condition: '' } },
);
directive = spectator.directive;
cdr = (directive as any).cdRef as ChangeDetectorRef;
});
it('should not call detectChanges method', () => {
const detectChanges = jest.spyOn(cdr, 'detectChanges');
const markForCheck = jest.spyOn(cdr, 'markForCheck');
expect(spectator.query('#test-element')).toBeFalsy();
spectator.setHostInput({ condition: 'test' });
grantedPolicy$.next(true);
expect(spectator.query('#test-element')).toBeTruthy();
expect(detectChanges).not.toHaveBeenCalled();
expect(markForCheck).toHaveBeenCalled();
grantedPolicy$.next(false);
expect(spectator.query('#test-element')).toBeFalsy();
expect(detectChanges).not.toHaveBeenCalled();
expect(markForCheck).toHaveBeenCalled();
grantedPolicy$.next(true);
expect(spectator.queryAll('#test-element')).toHaveLength(1);
expect(detectChanges).not.toHaveBeenCalled();
expect(markForCheck).toHaveBeenCalled();
});
});
});

6
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html

@ -1,4 +1,8 @@
<div class="mb-3 form-group" *abpPermission="prop.permission" [ngSwitch]="getComponent(prop)">
<div
class="mb-3 form-group"
*abpPermission="prop.permission; runChangeDetection: false"
[ngSwitch]="getComponent(prop)"
>
<ng-template ngSwitchCase="input">
<ng-template [ngTemplateOutlet]="label"></ng-template>
<input

2
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html

@ -24,7 +24,7 @@
[sortable]="prop.sortable"
>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission">
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *ngIf="row['_' + prop.name]?.visible">
<div
*ngIf="!row['_' + prop.name].component; else component"

4
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html

@ -27,7 +27,7 @@
<ng-container *ngIf="action.visible(data)">
<button
ngbDropdownItem
*abpPermission="action.permission"
*abpPermission="action.permission; runChangeDetection: false"
(click)="action.action(data)"
type="button"
>
@ -49,7 +49,7 @@
<ng-template #btnTmp let-action>
<ng-container *ngIf="action.visible(data)">
<button
*abpPermission="action.permission"
*abpPermission="action.permission; runChangeDetection: false"
(click)="action.action(data)"
type="button"
class="btn btn-primary text-center"

8
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html

@ -5,7 +5,7 @@
*ngFor="let action of actionList; trackBy: trackByFn; let last = last"
>
<ng-container *ngIf="action.visible(data)">
<ng-container *abpPermission="action.permission">
<ng-container *abpPermission="action.permission;runChangeDetection: false">
<ng-container *ngIf="action.component as component; else button">
<ng-container
*ngComponentOutlet="component; injector: record | createInjector: action:this"
@ -13,7 +13,11 @@
</ng-container>
<ng-template #button>
<button (click)="action.action(data)" type="button" [ngClass]="action.btnClass ? action.btnClass : defaultBtnClass" >
<button
(click)="action.action(data)"
type="button"
[ngClass]="action.btnClass ? action.btnClass : defaultBtnClass"
>
<i [ngClass]="action.icon" [class.me-1]="action.icon"></i>
{{ action.text | abpLocalization }}
</button>

Loading…
Cancel
Save