mirror of https://github.com/abpframework/abp.git
70 changed files with 568 additions and 683 deletions
@ -0,0 +1,249 @@ |
|||||
|
# Chart Component |
||||
|
|
||||
|
ABP Chart component exposed by `@abp/ng.components/chart.js` is based on [`charts.js`](https://www.chartjs.org/) v3+. You don't need to install the `chart.js` package. Since the `@abp/ng.components` is dependent on the `chart.js`, the package is already installed in your project. |
||||
|
|
||||
|
> Chart component loads `chart.js` script lazy. So it does not increase the bundle size. |
||||
|
|
||||
|
## How to Use |
||||
|
|
||||
|
First of all, need to import the `ChartModule` to your feature module as follows: |
||||
|
|
||||
|
```ts |
||||
|
// your-feature.module.ts |
||||
|
|
||||
|
import { ChartModule } from '@abp/ng.components/chart.js'; |
||||
|
import { ChartDemoComponent } from './chart-demo.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
ChartModule, |
||||
|
// ... |
||||
|
], |
||||
|
declarations: [ChartDemoComponent], |
||||
|
// ... |
||||
|
}) |
||||
|
export class YourFeatureModule {} |
||||
|
``` |
||||
|
|
||||
|
Then, `abp-chart` component can be used. See an example: |
||||
|
|
||||
|
```ts |
||||
|
// chart-demo.component.ts |
||||
|
|
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-chart-demo', |
||||
|
template: ` <abp-chart type="pie" [data]="data"></abp-chart> `, |
||||
|
}) |
||||
|
export class ChartDemoComponent { |
||||
|
data = { |
||||
|
labels: ['Data 1', 'Data 2', 'Data 3'], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: 'Dataset 1', |
||||
|
data: [40, 15, 45], |
||||
|
backgroundColor: ['#ff7675', '#fdcb6e', '#0984e3'], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> **Important Note**: Changing the chart data without creating a new data instance does not trigger change detection. In order to chart to redraw itself, a new data object needs to be created. |
||||
|
|
||||
|
See the result: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Examples |
||||
|
|
||||
|
### Doughnut |
||||
|
|
||||
|
```ts |
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-chart-demo', |
||||
|
template: ` |
||||
|
<abp-chart |
||||
|
type="doughnut" |
||||
|
[data]="data" |
||||
|
[options]="options" |
||||
|
width="400px" |
||||
|
height="400px" |
||||
|
></abp-chart> |
||||
|
`, |
||||
|
}) |
||||
|
export class ChartDemoComponent { |
||||
|
data = { |
||||
|
labels: ['Data 1', 'Data 2', 'Data 3'], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: 'Dataset 1', |
||||
|
data: [40, 15, 45], |
||||
|
backgroundColor: ['#a0e6c3', '#f0ea4c', '#5b9dc3'], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
options = { |
||||
|
plugins: { |
||||
|
title: { |
||||
|
display: true, |
||||
|
text: 'Doughnut Chart', |
||||
|
fontSize: 16, |
||||
|
}, |
||||
|
legend: { |
||||
|
position: 'bottom', |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Result: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Bar |
||||
|
|
||||
|
```ts |
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-chart-demo', |
||||
|
template: ` |
||||
|
<abp-chart |
||||
|
type="bar" |
||||
|
[data]="data" |
||||
|
width="400px" |
||||
|
height="400px" |
||||
|
></abp-chart> |
||||
|
`, |
||||
|
}) |
||||
|
export class ChartDemoComponent { |
||||
|
data = { |
||||
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: 'First dataset', |
||||
|
backgroundColor: '#42A5F5', |
||||
|
data: [65, 59, 80, 81, 56, 55, 40], |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Second dataset', |
||||
|
backgroundColor: '#FFA726', |
||||
|
data: [28, 48, 40, 19, 86, 27, 90], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Result: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Radar |
||||
|
|
||||
|
```ts |
||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-chart-demo', |
||||
|
template: ` |
||||
|
<abp-chart |
||||
|
type="radar" |
||||
|
[data]="data" |
||||
|
width="400px" |
||||
|
height="400px" |
||||
|
></abp-chart> |
||||
|
|
||||
|
<button class="btn btn-primary-outline mt-4" (click)="addDataset()"> |
||||
|
Add dataset |
||||
|
</button> |
||||
|
`, |
||||
|
}) |
||||
|
export class ChartDemoComponent { |
||||
|
data = { |
||||
|
labels: [ |
||||
|
'January', |
||||
|
'February', |
||||
|
'March', |
||||
|
'April', |
||||
|
'May', |
||||
|
'June', |
||||
|
'July', |
||||
|
'August', |
||||
|
'September', |
||||
|
'October', |
||||
|
'November', |
||||
|
'December', |
||||
|
], |
||||
|
datasets: [ |
||||
|
{ |
||||
|
label: 'Dataset 1', |
||||
|
backgroundColor: 'rgba(179,181,198,0.2)', |
||||
|
borderColor: 'rgba(179,181,198,1)', |
||||
|
data: [65, 59, 90, 81, 56, 55, 40, 35, 82, 51, 62, 95], |
||||
|
}, |
||||
|
{ |
||||
|
label: 'Dataset 2', |
||||
|
backgroundColor: 'rgba(255,99,132,0.2)', |
||||
|
borderColor: 'rgba(255,99,132,1)', |
||||
|
data: [28, 48, 40, 58, 96, 27, 100, 44, 85, 77, 71, 39], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
addDataset() { |
||||
|
this.data = { |
||||
|
...this.data, |
||||
|
datasets: [ |
||||
|
...this.data.datasets, |
||||
|
{ |
||||
|
label: 'Dataset 3', |
||||
|
backgroundColor: 'rgba(54,162,235,0.2)', |
||||
|
borderColor: 'rgba(54, 162, 235, 1)', |
||||
|
data: [90, 95, 98, 91, 99, 96, 89, 95, 98, 93, 92, 90], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Result: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
See the [`chart.js` samples](https://www.chartjs.org/docs/latest/samples) for more examples. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### `abp-chart` |
||||
|
|
||||
|
#### Properties |
||||
|
|
||||
|
| Name | Description | Type | Default | |
||||
|
| --------------- | ---------------------------------------------------------------- | ----------------------- | ------- | |
||||
|
| `[type]` | Type of the chart. | `string` | null | |
||||
|
| `[data]` | Chart data to display | `any` | null | |
||||
|
| `[options]` | Chart options to customize | `any` | null | |
||||
|
| `[plugins]` | Chart plugins to customize behaviour | `any` | null | |
||||
|
| `[width]` | Witdh of the chart | `string` | null | |
||||
|
| `[height]` | Height of the chart | `string` | null | |
||||
|
| `[responsive]` | Whether the chart is responsive | `boolean` | true | |
||||
|
| `(dataSelect)` | A callback that executes when an element on the chart is clicked | `EventEmitter<any>` | - | |
||||
|
| `(initialized)` | A callback that executes when the chart is initialized | `EventEmitter<boolean>` | - | |
||||
|
|
||||
|
#### Methods |
||||
|
|
||||
|
| Name | Description | Parameters | |
||||
|
| ---------------- | ------------------------------------------------------------------- | ---------- | |
||||
|
| `refresh` | Redraws the chart | - | |
||||
|
| `reinit` | Destroys the chart then creates it again | - | |
||||
|
| `getBase64Image` | Returns a base 64 encoded string of the chart in it's current state | - | |
||||
|
| `generateLegend` | Returns an HTML string of a legend for the chart | - | |
||||
|
| `getCanvas` | Returns the canvas HTML element | - | |
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
@ -0,0 +1,105 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Localization; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using Volo.Abp.Aspects; |
||||
|
using Volo.Abp.AspNetCore.Mvc.Validation; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Features; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.ObjectMapping; |
||||
|
using Volo.Abp.Timing; |
||||
|
using Volo.Abp.Uow; |
||||
|
using Volo.Abp.Users; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc |
||||
|
{ |
||||
|
public abstract class AbpControllerBase : ControllerBase, IAvoidDuplicateCrossCuttingConcerns |
||||
|
{ |
||||
|
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } |
||||
|
|
||||
|
protected IUnitOfWorkManager UnitOfWorkManager => LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>(); |
||||
|
|
||||
|
protected Type ObjectMapperContext { get; set; } |
||||
|
protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider => |
||||
|
ObjectMapperContext == null |
||||
|
? provider.GetRequiredService<IObjectMapper>() |
||||
|
: (IObjectMapper) provider.GetRequiredService(typeof(IObjectMapper<>).MakeGenericType(ObjectMapperContext))); |
||||
|
|
||||
|
protected IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetService<IGuidGenerator>(SimpleGuidGenerator.Instance); |
||||
|
|
||||
|
protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>(); |
||||
|
|
||||
|
protected ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance); |
||||
|
|
||||
|
protected ICurrentUser CurrentUser => LazyServiceProvider.LazyGetRequiredService<ICurrentUser>(); |
||||
|
|
||||
|
protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>(); |
||||
|
|
||||
|
protected IAuthorizationService AuthorizationService => LazyServiceProvider.LazyGetRequiredService<IAuthorizationService>(); |
||||
|
|
||||
|
protected IUnitOfWork CurrentUnitOfWork => UnitOfWorkManager?.Current; |
||||
|
|
||||
|
protected IClock Clock => LazyServiceProvider.LazyGetRequiredService<IClock>(); |
||||
|
|
||||
|
protected IModelStateValidator ModelValidator => LazyServiceProvider.LazyGetRequiredService<IModelStateValidator>(); |
||||
|
|
||||
|
protected IFeatureChecker FeatureChecker => LazyServiceProvider.LazyGetRequiredService<IFeatureChecker>(); |
||||
|
|
||||
|
protected IStringLocalizerFactory StringLocalizerFactory => LazyServiceProvider.LazyGetRequiredService<IStringLocalizerFactory>(); |
||||
|
|
||||
|
protected IStringLocalizer L |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
if (_localizer == null) |
||||
|
{ |
||||
|
_localizer = CreateLocalizer(); |
||||
|
} |
||||
|
|
||||
|
return _localizer; |
||||
|
} |
||||
|
} |
||||
|
private IStringLocalizer _localizer; |
||||
|
|
||||
|
protected Type LocalizationResource |
||||
|
{ |
||||
|
get => _localizationResource; |
||||
|
set |
||||
|
{ |
||||
|
_localizationResource = value; |
||||
|
_localizer = null; |
||||
|
} |
||||
|
} |
||||
|
private Type _localizationResource = typeof(DefaultResource); |
||||
|
|
||||
|
public List<string> AppliedCrossCuttingConcerns { get; } = new List<string>(); |
||||
|
|
||||
|
protected virtual IStringLocalizer CreateLocalizer() |
||||
|
{ |
||||
|
if (LocalizationResource != null) |
||||
|
{ |
||||
|
return StringLocalizerFactory.Create(LocalizationResource); |
||||
|
} |
||||
|
|
||||
|
var localizer = StringLocalizerFactory.CreateDefaultOrNull(); |
||||
|
if (localizer == null) |
||||
|
{ |
||||
|
throw new AbpException($"Set {nameof(LocalizationResource)} or define the default localization resource type (by configuring the {nameof(AbpLocalizationOptions)}.{nameof(AbpLocalizationOptions.DefaultResourceType)}) to be able to use the {nameof(L)} object!"); |
||||
|
} |
||||
|
|
||||
|
return localizer; |
||||
|
} |
||||
|
|
||||
|
protected virtual void ValidateModel() |
||||
|
{ |
||||
|
ModelValidator?.Validate(ModelState); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,53 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Confluent.Kafka; |
|
||||
using Microsoft.Extensions.Logging; |
|
||||
using Microsoft.Extensions.Logging.Abstractions; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.Data; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus.Kafka |
|
||||
{ |
|
||||
public class KafkaEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
|
||||
{ |
|
||||
protected ILogger<KafkaEventErrorHandler> Logger { get; set; } |
|
||||
|
|
||||
public KafkaEventErrorHandler( |
|
||||
IOptions<AbpEventBusOptions> options) : base(options) |
|
||||
{ |
|
||||
Logger = NullLogger<KafkaEventErrorHandler>.Instance; |
|
||||
} |
|
||||
|
|
||||
protected override async Task RetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
|
||||
{ |
|
||||
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
|
||||
} |
|
||||
|
|
||||
context.TryGetRetryAttempt(out var retryAttempt); |
|
||||
|
|
||||
await context.EventBus.As<KafkaDistributedEventBus>().PublishAsync( |
|
||||
context.EventType, |
|
||||
context.EventData, |
|
||||
context.GetProperty(HeadersKey).As<Headers>(), |
|
||||
new Dictionary<string, object> {{RetryAttemptKey, ++retryAttempt}}); |
|
||||
} |
|
||||
|
|
||||
protected override async Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
Logger.LogException( |
|
||||
context.Exceptions.Count == 1 ? context.Exceptions.First() : new AggregateException(context.Exceptions), |
|
||||
LogLevel.Error); |
|
||||
|
|
||||
await context.EventBus.As<KafkaDistributedEventBus>().PublishToDeadLetterAsync( |
|
||||
context.EventType, |
|
||||
context.EventData, |
|
||||
context.GetProperty(HeadersKey).As<Headers>(), |
|
||||
new Dictionary<string, object> {{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()}}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using RabbitMQ.Client; |
|
||||
using Volo.Abp.Data; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus.RabbitMq |
|
||||
{ |
|
||||
public class RabbitMqEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
|
||||
{ |
|
||||
public RabbitMqEventErrorHandler( |
|
||||
IOptions<AbpEventBusOptions> options) |
|
||||
: base(options) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override async Task RetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
|
||||
{ |
|
||||
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
|
||||
} |
|
||||
|
|
||||
context.TryGetRetryAttempt(out var retryAttempt); |
|
||||
|
|
||||
await context.EventBus.As<RabbitMqDistributedEventBus>().PublishAsync( |
|
||||
context.EventType, |
|
||||
context.EventData, |
|
||||
context.GetProperty(HeadersKey).As<IBasicProperties>(), |
|
||||
new Dictionary<string, object> |
|
||||
{ |
|
||||
{RetryAttemptKey, ++retryAttempt}, |
|
||||
{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
ThrowOriginalExceptions(context); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,32 +0,0 @@ |
|||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus.Rebus |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Rebus will automatic retries and error handling: https://github.com/rebus-org/Rebus/wiki/Automatic-retries-and-error-handling
|
|
||||
/// </summary>
|
|
||||
public class RebusEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
|
||||
{ |
|
||||
public RebusEventErrorHandler( |
|
||||
IOptions<AbpEventBusOptions> options) |
|
||||
: base(options) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
protected override Task RetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
ThrowOriginalExceptions(context); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
ThrowOriginalExceptions(context); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public class AbpEventBusOptions |
|
||||
{ |
|
||||
public bool EnabledErrorHandle { get; set; } |
|
||||
|
|
||||
public Func<Type, bool> ErrorHandleSelector { get; set; } |
|
||||
|
|
||||
public string DeadLetterName { get; set; } |
|
||||
|
|
||||
public AbpEventBusRetryStrategyOptions RetryStrategyOptions { get; set; } |
|
||||
|
|
||||
public void UseRetryStrategy(Action<AbpEventBusRetryStrategyOptions> action = null) |
|
||||
{ |
|
||||
EnabledErrorHandle = true; |
|
||||
RetryStrategyOptions = new AbpEventBusRetryStrategyOptions(); |
|
||||
action?.Invoke(RetryStrategyOptions); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public class AbpEventBusRetryStrategyOptions |
|
||||
{ |
|
||||
public int IntervalMillisecond { get; set; } = 3000; |
|
||||
|
|
||||
public int MaxRetryAttempts { get; set; } = 3; |
|
||||
} |
|
||||
} |
|
||||
@ -1,76 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public abstract class EventErrorHandlerBase : IEventErrorHandler |
|
||||
{ |
|
||||
public const string HeadersKey = "headers"; |
|
||||
public const string RetryAttemptKey = "retryAttempt"; |
|
||||
|
|
||||
protected AbpEventBusOptions Options { get; } |
|
||||
|
|
||||
protected EventErrorHandlerBase(IOptions<AbpEventBusOptions> options) |
|
||||
{ |
|
||||
Options = options.Value; |
|
||||
} |
|
||||
|
|
||||
public virtual async Task HandleAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (!await ShouldHandleAsync(context)) |
|
||||
{ |
|
||||
ThrowOriginalExceptions(context); |
|
||||
} |
|
||||
|
|
||||
if (await ShouldRetryAsync(context)) |
|
||||
{ |
|
||||
await RetryAsync(context); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
await MoveToDeadLetterAsync(context); |
|
||||
} |
|
||||
|
|
||||
protected abstract Task RetryAsync(EventExecutionErrorContext context); |
|
||||
|
|
||||
protected abstract Task MoveToDeadLetterAsync(EventExecutionErrorContext context); |
|
||||
|
|
||||
protected virtual Task<bool> ShouldHandleAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (!Options.EnabledErrorHandle) |
|
||||
{ |
|
||||
return Task.FromResult(false); |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult(Options.ErrorHandleSelector == null || Options.ErrorHandleSelector.Invoke(context.EventType)); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (Options.RetryStrategyOptions == null) |
|
||||
{ |
|
||||
return Task.FromResult(false); |
|
||||
} |
|
||||
|
|
||||
if (!context.TryGetRetryAttempt(out var retryAttempt)) |
|
||||
{ |
|
||||
return Task.FromResult(false); |
|
||||
} |
|
||||
|
|
||||
return Task.FromResult(Options.RetryStrategyOptions.MaxRetryAttempts > retryAttempt); |
|
||||
} |
|
||||
|
|
||||
protected virtual void ThrowOriginalExceptions(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (context.Exceptions.Count == 1) |
|
||||
{ |
|
||||
context.Exceptions[0].ReThrow(); |
|
||||
} |
|
||||
|
|
||||
throw new AggregateException( |
|
||||
"More than one error has occurred while triggering the event: " + context.EventType, |
|
||||
context.Exceptions); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,38 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Volo.Abp.Data; |
|
||||
using Volo.Abp.ObjectExtending; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public class EventExecutionErrorContext : ExtensibleObject |
|
||||
{ |
|
||||
public IReadOnlyList<Exception> Exceptions { get; } |
|
||||
|
|
||||
public object EventData { get; set; } |
|
||||
|
|
||||
public Type EventType { get; } |
|
||||
|
|
||||
public IEventBus EventBus { get; } |
|
||||
|
|
||||
public EventExecutionErrorContext(List<Exception> exceptions, Type eventType, IEventBus eventBus) |
|
||||
{ |
|
||||
Exceptions = exceptions; |
|
||||
EventType = eventType; |
|
||||
EventBus = eventBus; |
|
||||
} |
|
||||
|
|
||||
public bool TryGetRetryAttempt(out int retryAttempt) |
|
||||
{ |
|
||||
retryAttempt = 0; |
|
||||
if (!this.HasProperty(EventErrorHandlerBase.RetryAttemptKey)) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
retryAttempt = this.GetProperty<int>(EventErrorHandlerBase.RetryAttemptKey); |
|
||||
return true; |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
using System.Threading.Tasks; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public interface IEventErrorHandler |
|
||||
{ |
|
||||
Task HandleAsync(EventExecutionErrorContext context); |
|
||||
} |
|
||||
} |
|
||||
@ -1,60 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.Data; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus.Local |
|
||||
{ |
|
||||
[ExposeServices(typeof(LocalEventErrorHandler), typeof(IEventErrorHandler))] |
|
||||
public class LocalEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
|
||||
{ |
|
||||
protected Dictionary<Guid, int> RetryTracking { get; } |
|
||||
|
|
||||
public LocalEventErrorHandler( |
|
||||
IOptions<AbpEventBusOptions> options) |
|
||||
: base(options) |
|
||||
{ |
|
||||
RetryTracking = new Dictionary<Guid, int>(); |
|
||||
} |
|
||||
|
|
||||
protected override async Task RetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
|
||||
{ |
|
||||
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
|
||||
} |
|
||||
|
|
||||
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
|
||||
|
|
||||
context.TryGetRetryAttempt(out var retryAttempt); |
|
||||
RetryTracking[messageId] = ++retryAttempt; |
|
||||
|
|
||||
await context.EventBus.As<LocalEventBus>().PublishAsync(new LocalEventMessage(messageId, context.EventData, context.EventType)); |
|
||||
|
|
||||
RetryTracking.Remove(messageId); |
|
||||
} |
|
||||
|
|
||||
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
ThrowOriginalExceptions(context); |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
protected override async Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
|
||||
{ |
|
||||
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
|
||||
context.SetProperty(RetryAttemptKey, RetryTracking.GetOrDefault(messageId)); |
|
||||
|
|
||||
if (await base.ShouldRetryAsync(context)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
RetryTracking.Remove(messageId); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,72 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Shouldly; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Volo.Abp.EventBus.Local |
|
||||
{ |
|
||||
public class EventBus_Exception_Handler_Tests : EventBusTestBase |
|
||||
{ |
|
||||
[Fact] |
|
||||
public async Task Should_Not_Handle_Exception() |
|
||||
{ |
|
||||
var retryAttempt = 0; |
|
||||
LocalEventBus.Subscribe<MySimpleEventData>(eventData => |
|
||||
{ |
|
||||
retryAttempt++; |
|
||||
throw new Exception("This exception is intentionally thrown!"); |
|
||||
}); |
|
||||
|
|
||||
var appException = await Assert.ThrowsAsync<Exception>(async () => |
|
||||
{ |
|
||||
await LocalEventBus.PublishAsync(new MySimpleEventData(1)); |
|
||||
}); |
|
||||
|
|
||||
retryAttempt.ShouldBe(1); |
|
||||
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_Handle_Exception() |
|
||||
{ |
|
||||
var retryAttempt = 0; |
|
||||
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
|
||||
{ |
|
||||
eventData.Value.ShouldBe(0); |
|
||||
retryAttempt++; |
|
||||
if (retryAttempt < 2) |
|
||||
{ |
|
||||
throw new Exception("This exception is intentionally thrown!"); |
|
||||
} |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
|
|
||||
}); |
|
||||
|
|
||||
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
|
||||
retryAttempt.ShouldBe(2); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_Throw_Exception_After_Error_Handle() |
|
||||
{ |
|
||||
var retryAttempt = 0; |
|
||||
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
|
||||
{ |
|
||||
eventData.Value.ShouldBe(0); |
|
||||
|
|
||||
retryAttempt++; |
|
||||
|
|
||||
throw new Exception("This exception is intentionally thrown!"); |
|
||||
}); |
|
||||
|
|
||||
var appException = await Assert.ThrowsAsync<Exception>(async () => |
|
||||
{ |
|
||||
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
|
||||
}); |
|
||||
|
|
||||
retryAttempt.ShouldBe(4); |
|
||||
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
namespace Volo.Abp.EventBus |
|
||||
{ |
|
||||
public class MyExceptionHandleEventData |
|
||||
{ |
|
||||
public int Value { get; set; } |
|
||||
|
|
||||
public MyExceptionHandleEventData(int value) |
|
||||
{ |
|
||||
Value = value; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,23 @@ |
|||||
|
<ol class="breadcrumb" *ngIf="items.length"> |
||||
|
<li class="breadcrumb-item"> |
||||
|
<a routerLink="/"><i class="fa fa-home"></i> </a> |
||||
|
</li> |
||||
|
<li |
||||
|
*ngFor="let item of items; let last = last" |
||||
|
class="breadcrumb-item" |
||||
|
[class.active]="last" |
||||
|
aria-current="page" |
||||
|
> |
||||
|
<ng-container |
||||
|
*ngTemplateOutlet="item.path ? linkTemplate : textTemplate; context: { $implicit: item }" |
||||
|
></ng-container> |
||||
|
</li> |
||||
|
</ol> |
||||
|
|
||||
|
<ng-template #linkTemplate let-item> |
||||
|
<a [routerLink]="item.path"> {{ item.name | abpLocalization }}</a> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #textTemplate let-item> |
||||
|
{{ item.name | abpLocalization }} |
||||
|
</ng-template> |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { Component, Input } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'abp-breadcrumb-items', |
||||
|
templateUrl: './breadcrumb-items.component.html', |
||||
|
}) |
||||
|
export class BreadcrumbItemsComponent { |
||||
|
@Input() items = []; |
||||
|
} |
||||
@ -1,13 +1 @@ |
|||||
<ol class="breadcrumb" *ngIf="segments.length"> |
<abp-breadcrumb-items [items]="segments"></abp-breadcrumb-items> |
||||
<li class="breadcrumb-item"> |
|
||||
<a routerLink="/"><i class="fa fa-home"></i> </a> |
|
||||
</li> |
|
||||
<li |
|
||||
*ngFor="let segment of segments; let last = last" |
|
||||
class="breadcrumb-item" |
|
||||
[class.active]="last" |
|
||||
aria-current="page" |
|
||||
> |
|
||||
{{ segment.name | abpLocalization }} |
|
||||
</li> |
|
||||
</ol> |
|
||||
|
|||||
Loading…
Reference in new issue