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"> |
|||
<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> |
|||
<abp-breadcrumb-items [items]="segments"></abp-breadcrumb-items> |
|||
|
|||
Loading…
Reference in new issue