diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index 0e0a530416..6dc4ccbd9f 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -1,4 +1,4 @@ -## Exception Handling +# Exception Handling ABP provides a built-in infrastructure and offers a standard model for handling exceptions in a web application. @@ -7,7 +7,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling * Provides a configurable way to **localize** exception messages. * Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map these to custom exceptions. -### Automatic Exception Handling +## Automatic Exception Handling `AbpExceptionFilter` handles an exception if **any of the following conditions** are met: @@ -17,7 +17,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client. -#### Error Message Format +### Error Message Format Error Message is an instance of the `RemoteServiceErrorResponse` class. The simplest error JSON has a **message** property as shown below: @@ -83,11 +83,11 @@ Error **details** in an optional field of the JSON error message. Thrown `Except `AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic. -#### Logging +### Logging Caught exceptions are automatically logged. -##### Log Level +#### Log Level Exceptions are logged with the `Error` level by default. The Log level can be determined by the exception if it implements the `IHasLogLevel` interface. Example: @@ -100,7 +100,7 @@ public class MyException : Exception, IHasLogLevel } ```` -##### Self Logging Exceptions +#### Self Logging Exceptions Some exception types may need to write additional logs. They can implement the `IExceptionWithSelfLogging` if needed. Example: @@ -116,7 +116,7 @@ public class MyException : Exception, IExceptionWithSelfLogging > `ILogger.LogException` extension methods is used to write exception logs. You can use the same extension method when needed. -### Business Exceptions +## Business Exceptions Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception. @@ -145,11 +145,11 @@ Volo.Qa:010002 * You can **directly throw** a `BusinessException` or **derive** your own exception types from it when needed. * All properties are optional for the `BusinessException` class. But you generally set either `ErrorCode` or `Message` property. -### Exception Localization +## Exception Localization One problem with throwing exceptions is how to localize error messages while sending it to the client. ABP offers two models and their variants. -#### User Friendly Exception +### User Friendly Exception If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client. @@ -192,7 +192,7 @@ Then the localization text can be: * The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class. -#### Using Error Codes +### Using Error Codes `UserFriendlyException` is fine, but it has a few problems in advanced usages: @@ -230,7 +230,7 @@ throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer); * Throwing any exception implementing the `IHasErrorCode` interface behaves the same. So, the error code localization approach is not unique to the `BusinessException` class. * Defining localized string is not required for an error message. If it's not defined, ABP sends the default error message to the client. It does not use the `Message` property of the exception! if you want that, use the `UserFriendlyException` (or use an exception type that implements the `IUserFriendlyException` interface). -##### Using Message Parameters +#### Using Message Parameters If you have a parameterized error message, then you can set it with the exception's `Data` property. For example: @@ -265,7 +265,7 @@ Then the localized text can contain the `UserName` parameter: * `WithData` can be chained with more than one parameter (like `.WithData(...).WithData(...)`). -### HTTP Status Code Mapping +## HTTP Status Code Mapping ABP tries to automatically determine the most suitable HTTP status code for common exception types by following these rules: @@ -280,7 +280,7 @@ ABP tries to automatically determine the most suitable HTTP status code for comm The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP status code. The default implementation is the `DefaultHttpExceptionStatusCodeFinder` class. It can be replaced or extended as needed. -#### Custom Mappings +### Custom Mappings Automatic HTTP status code determination can be overrided by custom mappings. For example: @@ -291,7 +291,27 @@ services.Configure(options => }); ```` -### Built-In Exceptions +## Subscribing to the Exceptions + +It is possible to be informed when the ABP Framework **handles an exception**. It automatically **logs** all the exceptions to the standard [logger](Logging.md), but you may want to do more. + +In this case, create a class derived from the `ExceptionSubscriber` class in your application: + +````csharp +public class MyExceptionSubscriber : ExceptionSubscriber +{ + public override async Task HandleAsync(ExceptionNotificationContext context) + { + //TODO... + } +} +```` + +The `context` object contains necessary information about the exception occurred. + +> You can have multiple subscribers, each gets a copy of the exception. Exceptions thrown by your subscriber is ignored (but still logged). + +## Built-In Exceptions Some exception types are automatically thrown by the framework: diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 1e1e4e9f81..27fe70086c 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -4,7 +4,7 @@ ABP Multi-tenancy module provides base functionality to create multi tenant appl Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that: -> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of a software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants. +> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants. ### Volo.Abp.MultiTenancy Package diff --git a/docs/en/UI/Angular/Custom-Setting-Page.md b/docs/en/UI/Angular/Custom-Setting-Page.md index 7c51cf2cca..6aa5d8a3cc 100644 --- a/docs/en/UI/Angular/Custom-Setting-Page.md +++ b/docs/en/UI/Angular/Custom-Setting-Page.md @@ -40,3 +40,7 @@ ngOnInit() { Navigate to `/setting-management` route to see the changes: ![Custom Settings Tab](./images/custom-settings.png) + +## What's Next? + +- [TrackByService](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Http-Requests.md b/docs/en/UI/Angular/Http-Requests.md new file mode 100644 index 0000000000..71878c8045 --- /dev/null +++ b/docs/en/UI/Angular/Http-Requests.md @@ -0,0 +1,209 @@ +# How to Make HTTP Requests + + + +## About HttpClient + +Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project. + +However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned. + +```js +getConfig() { + this.http.get(this.configUrl).subscribe( + config => this.updateConfig(config), + error => { + // Handle error here + }, + ); +} +``` + +Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable. + +An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details. + + + +## RestService + +ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult. + + + +### Getting Started with RestService + +In order to use the `RestService`, you must inject it in your class as a dependency. + +```js +import { RestService } from '@abp/ng.core'; + +@Injectable({ + /* class metadata here */ +}) +class DemoService { + constructor(private rest: RestService) {} +} +``` + +You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**. + + + +### How to Make a Request with RestService + +You can use the `request` method of the `RestService` is for HTTP requests. Here is an example: + +```js +getFoo(id: number) { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request); +} +``` + + + +The `request` method always returns an `Observable`. Therefore you can do the following wherever you use `getFoo` method: + +```js +doSomethingWithFoo(id: number) { + this.demoService.getFoo(id).subscribe( + foo => { + // Do something with foo. + } + ) +} +``` + + + +**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error. + + + +As you see, `request` method gets a request options object with `Rest.Request` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one: + +```js +postFoo(body: Foo) { + const request: Rest.Request = { + method: 'POST', + url: '/api/some/path/to/foo', + body + }; + + return this.rest.request(request); +} +``` + + + +You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular. + + + +### How to Disable Default Error Handler of RestService + +The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself: + +```js +deleteFoo(id: number) { + const request: Rest.Request = { + method: 'DELETE', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request, { skipHandleError: true }); +} +``` + + + +`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription. + +```js +removeFooFromList(id: number) { + this.demoService.deleteFoo(id).subscribe( + foo => { + // Do something with foo. + }, + error => { + // Do something with error. + } + ) +} +``` + + + +### How to Get a Specific API Endpoint From Application Config + +Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration. + + + +```js +putFoo(body: Foo, id: string) { + const request: Rest.Request = { + method: 'PUT', + url: '/' + id, + body + }; + + return this.rest.request(request, {apiName: 'foo'}); +} +``` + + + +`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows: + +```js +// environment.ts + +export const environment = { + apis: { + default: { + url: 'https://localhost:44305', + }, + foo: { + url: 'https://localhost:44305/api/some/path/to/foo', + }, + }, + + /* rest of the environment variables here */ +} +``` + + + +### How to Observe Response Object or HTTP Events Instead of Body + +`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object. + +```js +getSomeCustomHeaderValue() { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/that/sends/some-custom-header', + }; + + return this.rest.request>( + request, + {observe: Rest.Observe.Response}, + ).pipe( + map(response => response.headers.get('Some-Custom-Header')) + ); +} +``` + + + +You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). + +## What's Next? + +* [Localization](./Localization.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md index 01394a3fea..d627f466ba 100644 --- a/docs/en/UI/Angular/Localization.md +++ b/docs/en/UI/Angular/Localization.md @@ -133,4 +133,8 @@ Localization resources are stored in the `localization` property of `ConfigState ## See Also -* [Localization in ASP.NET Core](../../Localization.md) \ No newline at end of file +* [Localization in ASP.NET Core](../../Localization.md) + +## What's Next? + +* [Permission Management](./Permission-Management.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md new file mode 100644 index 0000000000..4506f77e47 --- /dev/null +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -0,0 +1,114 @@ +# Easy TrackByFunction Implementation + +`TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. + + + +## Getting Started + +You do not have to provide the `TrackByService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. For better type support, you may pass in the type of the iterated item to it. + +```js +import { TrackByService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + list: Item[]; + + constructor(public readonly track: TrackByService) {} +} +``` + + + +> Noticed `track` is `public` and `readonly`? That is because we will see some examples where methods of `TrackByService` instance are directly called in the component's template. That may be considered as an anti-pattern, but it has its own advantage, especially when component inheritance is leveraged. You can always use public component properties instead. + + + +**The members are also exported as separate functions.** If you do not want to inject `TrackByService`, you can always import and use those functions directly in your classes. + + + +## Usage + +There are two approaches available. + +1. You may inject `TrackByService` to your component and use its members. +2. You may use exported higher-order functions directly on component properties. + + + +### How to Track Items by a Key + +You can use `by` to get a `TrackByFunction` that tracks the iterated object based on one of its keys. For type support, you may pass in the type of the iterated item to it. + +```html + + +
{%{{{ item.name }}}%}
+``` + + + +`by` is exported as a stand-alone function and is named `trackBy`. + +```js +import { trackBy } from "@abp/ng.core"; + +@Component({ + template: ` +
+ {%{{{ item.name }}}%} +
+ `, +}) +class DemoComponent { + list: Item[]; + + trackById = trackBy('id'); +} +``` + + + +### How to Track by a Deeply Nested Key + +You can use `byDeep` to get a `TrackByFunction` that tracks the iterated object based on a deeply nested key. For type support, you may pass in the type of the iterated item to it. + +```html + + +
+ {%{{{ item.tenant.name }}}%} +
+``` + + + +`byDeep` is exported as a stand-alone function and is named `trackByDeep`. + +```js +import { trackByDeep } from "@abp/ng.core"; + +@Component({ + template: ` +
+ {%{{{ item.name }}}%} +
+ `, +}) +class DemoComponent { + list: Item[]; + + trackByTenantAccountId = trackByDeep('tenant', 'account', 'id'); +} +``` + diff --git a/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md b/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md new file mode 100644 index 0000000000..a5a66e9e19 --- /dev/null +++ b/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md @@ -0,0 +1,3 @@ +# ABP Datatables.Net Integration for ASP.NET Core UI + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md new file mode 100644 index 0000000000..38fab4d44b --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md @@ -0,0 +1,92 @@ +# Collapse + +## Introduction + +`abp-collapse-body` is the main container for showing and hiding content. `abp-collapse-id` is used to show and hide the content container. Can be triggered with both `abp-button` and `a` tags. + +Basic usage: + +````xml + + Link with href + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + +```` + + + +## Demo + +See the [collapse demo page](https://bootstrap-taghelpers.abp.io/Components/Collapse) to see it in action. + +## Attributes + +### show + +A value indicates if the collapse body will be initialized visible or hidden. Should be one of the following values: + +* `false` (default value) +* `true` + +### multi + +A value indicates if an `abp-collapse-body` can be shown or hidden by an element that can show/hide multiple collapse bodies. Basically, this attribute adds "multi-collapse" class to `abp-collapse-body`. Should be one of the following values: + +* `false` (default value) +* `true` + +Sample: + +````xml + Toggle first element + + + + + + + Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit + + + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. + + + +```` + +## Accordion example + +`abp-accordion` is the main container for the accordion items. + +Basic usage: + +````xml + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + +```` + +## Attributes + +### active + +A value indicates if the accordion item will be initialized visible or hidden. Should be one of the following values: + +* `false` (default value) +* `true` + +### title + +A value indicates the visible title of the accordion item. Should be a string value. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md new file mode 100644 index 0000000000..1c062b5f73 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md @@ -0,0 +1,97 @@ +# Dropdowns + +## Introduction + +`abp-dropdown` is the main container for dropdown content. + +Basic usage: + +````xml + + + + Action + Another action + Something else here + + +```` + + + +## Demo + +See the [dropdown demo page](https://bootstrap-taghelpers.abp.io/Components/Dropdowns) to see it in action. + +## Attributes + +### direction + +A value indicates which direction the dropdown buttons will be displayed to. Should be one of the following values: + +* `Down` (default value) +* `Up` +* `Right` +* `Left` + +### dropdown-style + +A value indicates if an `abp-dropdown-button` will have split icon for dropdown. Should be one of the following values: + +* `Single` (default value) +* `Split` + + + +## Menu items + +`abp-dropdown-menu` is the main container for dropdown menu items. + +Basic usage: + +````xml + + + + Dropdown Header + Action + Active action + Disabled action + + Dropdown Item Text + Something else here + + +```` + +## Attributes + +### align + +A value indicates which direction `abp-dropdown-menu` items will be aligned to. Should be one of the following values: + +* `Left` (default value) +* `Right` + +### Additional content + +`abp-dropdown-menu` can also contain additional HTML elements like headings, paragraphs, dividers or form element. + +Example: + +````xml + + + +
+ + + + + + + New around here? Sign up + Forgot password? +
+
+```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md index 5f36395c00..a7aa0b65d2 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -15,6 +15,16 @@ Here, the list of components those are wrapped by the ABP Framework: * [Buttons](Buttons.md) * [Cards](Cards.md) * [Alerts](Alerts.md) +* [Tabs](Tabs.md) +* [Grids](Grids.md) +* [Modals](Modals.md) +* [Collapse](Collapse.md) +* [Dropdowns](Dropdowns.md) +* [List Groups](List-Groups.md) +* [Paginator](Paginator.md) +* [Popovers](Popovers.md) +* [Progress Bars](Progress-Bars.md) +* [Tooltips](Tooltips.md) * ... > Until all the tag helpers are documented, you can visit https://bootstrap-taghelpers.abp.io/ to see them with live samples. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md new file mode 100644 index 0000000000..b1e6e7f499 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md @@ -0,0 +1,78 @@ +# List Groups + +## Introduction + +`abp-list-group` is the main container for list group content. + +Basic usage: + +````xml + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + +```` + + + +## Demo + +See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroups) to see it in action. + +## Attributes + +### flush + +A value indicates `abp-list-group` items to remove some borders and rounded corners to render list group items edge-to-edge in a parent container. Should be one of the following values: + +* `false` (default value) +* `true` + +### active + +A value indicates if an `abp-list-group-item` to be active. Should be one of the following values: + +* `false` (default value) +* `true` + +### disabled + +A value indicates if an `abp-list-group-item` to be disabled. Should be one of the following values: + +* `false` (default value) +* `true` + +### href + +A value indicates if an `abp-list-group-item` has a link. Should be a string link value. + +### type + +A value indicates an `abp-list-group-item` style class with a stateful background and color. Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Link` + +### Additional content + +`abp-list-group-item` can also contain additional HTML elements like spans. + +Example: + +````xml + + Cras justo odio 14 + Dapibus ac facilisis in 2 + Morbi leo risus 1 + +```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md new file mode 100644 index 0000000000..9cf1258373 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md @@ -0,0 +1,81 @@ +# Modals + +## Introduction + +`abp-modal` is a main element to create a modal. + +Basic usage: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + + + +## Demo + +See the [modals demo page](https://bootstrap-taghelpers.abp.io/Components/Modals) to see it in action. + +## Attributes + +### centered + +A value indicates the positioning of the modal. Should be one of the following values: + +* `false` (default value) +* `true` + +### size + +A value indicates the size of the modal. Should be one of the following values: + +* `Default` (default value) +* `Small` +* `Large` +* `ExtraLarge` + +### static + +A value indicates if the modal will be static. Should be one of the following values: + +* `false` (default value) +* `true` + +### Additional content + +`abp-modal-footer` can have multiple buttons with alignment option. + +Add `@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal` to your page. + +Example: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + +### button-alignment + +A value indicates the positioning of your modal footer buttons. Should be one of the following values: + +* `Default` (default value) +* `Start` +* `Center` +* `Around` +* `Between` +* `End` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md new file mode 100644 index 0000000000..0cc63d49e8 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -0,0 +1,57 @@ +# Paginator + +## Introduction + +`abp-paginator` is the abp tag for pagination. Requires `Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model. + +Basic usage: + +````xml + +```` + +Model: + +````xml +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components +{ + public class PaginatorModel : PageModel + { + public PagerModel PagerModel { get; set; } + + public void OnGet(int currentPage, string sort) + { + PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + } + } +} +```` + + + +## Demo + +See the [paginator demo page](https://bootstrap-taghelpers.abp.io/Components/Paginator) to see it in action. + +## Attributes + +### model + +`Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model can be initialized with the following data: + +* `totalCount` +* `shownItemsCount` +* `currentPage` +* `pageSize` +* `pageUrl` +* `sort` (default null) + +### show-info + +A value indicates if an extra information about start, end and total records will be displayed. Should be one of the following values: + +* `false` (default value) +* `true` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md new file mode 100644 index 0000000000..dc69f7e47e --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md @@ -0,0 +1,70 @@ +# Progress Bars + +## Introduction + +`abp-progress-bar` is the abp tag for progress bar status. + +Basic usage: + +````xml + + + %25 + + + + %50 + + + + %10 + + +```` + + + +## Demo + +See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars) to see it in action. + +## Attributes + +### value + +A value indicates the current progress of the bar. + +### type + +A value indicates the background color of the progress bar. Should be one of the following values: + +* `Default` (default value) +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +### min-value + +Minimum value of the progress bar. Default is 0. + +### max-value + +Maximum value of the progress bar. Default is 100. + +### strip + +A value indicates if the background style of the progress bar is stripped. Should be one of the following values: + +* `false` (default value) +* `true` + +### animation + +A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values: + +* `false` (default value) +* `true` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md b/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md new file mode 100644 index 0000000000..55b6e574e8 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md @@ -0,0 +1,35 @@ +# Tooltips + +## Introduction + +`abp-tooltip` is the abp tag for tooltips. + +Basic usage: + +````xml + + Tooltip Default + + + + Tooltip on top + + + + Tooltip on right + + + + Tooltip on bottom + + + + Disabled button Tooltip + +```` + + + +## Demo + +See the [tooltips demo page](https://bootstrap-taghelpers.abp.io/Components/Tooltips) to see it in action. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index c273e59346..425b8c526a 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -302,12 +302,20 @@ { "text": "Theming", "path": "UI/AspNetCore/Theming.md" + }, + { + "text": "Customize/Extend the UI", + "path": "UI/AspNetCore/Customization-User-Interface.md" } ] }, { "text": "Angular", "items": [ + { + "text": "HTTP Requests", + "path": "UI/Angular/HTTP-Requests.md" + }, { "text": "Localization", "path": "UI/Angular/Localization.md" @@ -327,6 +335,10 @@ { "text": "Custom Setting Page", "path": "UI/Angular/Custom-Setting-Page.md" + }, + { + "text": "TrackByService", + "path": "UI/Angular/Track-By-Service.md" } ] } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs index 1fb4c12cf2..9682ec838e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; @@ -7,6 +8,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Views.Error; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers { @@ -16,20 +18,23 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers private readonly IHttpExceptionStatusCodeFinder _statusCodeFinder; private readonly IStringLocalizer _localizer; private readonly AbpErrorPageOptions _abpErrorPageOptions; + private readonly IExceptionNotifier _exceptionNotifier; public ErrorController( IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IHttpExceptionStatusCodeFinder httpExceptionStatusCodeFinder, IOptions abpErrorPageOptions, - IStringLocalizer localizer) + IStringLocalizer localizer, + IExceptionNotifier exceptionNotifier) { _errorInfoConverter = exceptionToErrorInfoConverter; _statusCodeFinder = httpExceptionStatusCodeFinder; _localizer = localizer; + _exceptionNotifier = exceptionNotifier; _abpErrorPageOptions = abpErrorPageOptions.Value; } - public IActionResult Index(int httpStatusCode) + public async Task Index(int httpStatusCode) { var exHandlerFeature = HttpContext.Features.Get(); @@ -37,6 +42,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers ? exHandlerFeature.Error : new Exception(_localizer["UnhandledException"]); + await _exceptionNotifier.NotifyAsync(new ExceptionNotificationContext(exception)); + var errorInfo = _errorInfoConverter.Convert(exception); if (httpStatusCode == 0) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs index 28c1c18fda..1ce177ba44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs @@ -1,18 +1,21 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { - public class AbpExceptionFilter : IExceptionFilter, ITransientDependency + public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency { public ILogger Logger { get; set; } @@ -32,14 +35,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger = NullLogger.Instance; } - public virtual void OnException(ExceptionContext context) + public async Task OnExceptionAsync(ExceptionContext context) { if (!ShouldHandleException(context)) { return; } - HandleAndWrapException(context); + await HandleAndWrapException(context); } protected virtual bool ShouldHandleException(ExceptionContext context) @@ -65,7 +68,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling return false; } - protected virtual void HandleAndWrapException(ExceptionContext context) + protected virtual async Task HandleAndWrapException(ExceptionContext context) { //TODO: Trigger an AbpExceptionHandled event or something like that. @@ -82,6 +85,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger.LogWithLevel(logLevel, _jsonSerializer.Serialize(remoteServiceErrorInfo, indented: true)); Logger.LogException(context.Exception, logLevel); + await context.HttpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(context.Exception) + ); + context.Exception = null; //Handled! } } diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs index cdd921dae4..9a7a8f1676 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.Net.Http.Headers; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Uow; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; @@ -73,6 +74,13 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling ) ) ); + + await httpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(exception) + ); } private Task ClearCacheHeaders(object state) diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs index d4ce5cd03a..a5027daec6 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.BackgroundJobs { @@ -51,6 +53,10 @@ namespace Volo.Abp.BackgroundJobs { Logger.LogException(ex); + await context.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + throw new BackgroundJobExecutionException("A background job execution is failed. See inner exception for details.", ex) { JobType = context.JobType.AssemblyQualifiedName, diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index 4cb4f04a09..879cf4f8ff 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Nito.AsyncEx; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using Volo.Abp.ExceptionHandling; using Volo.Abp.RabbitMQ; using Volo.Abp.Threading; @@ -31,6 +32,7 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ protected IRabbitMqSerializer Serializer { get; } protected IBackgroundJobExecuter JobExecuter { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } + protected IExceptionNotifier ExceptionNotifier { get; } protected SemaphoreSlim SyncObj = new SemaphoreSlim(1, 1); protected bool IsDiposed { get; private set; } @@ -41,13 +43,15 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ IChannelPool channelPool, IRabbitMqSerializer serializer, IBackgroundJobExecuter jobExecuter, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IExceptionNotifier exceptionNotifier) { AbpBackgroundJobOptions = backgroundJobOptions.Value; AbpRabbitMqBackgroundJobOptions = rabbitMqAbpBackgroundJobOptions.Value; Serializer = serializer; JobExecuter = jobExecuter; ServiceScopeFactory = serviceScopeFactory; + ExceptionNotifier = exceptionNotifier; ChannelPool = channelPool; JobConfiguration = AbpBackgroundJobOptions.GetJob(typeof(TArgs)); diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs index 62da163eb9..cb2f6dbfae 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -35,18 +36,24 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { AsyncHelper.RunSync( () => DoWorkAsync(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)) ); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + AsyncHelper.RunSync( + () => scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)) + ); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs index 091bcc917f..c63adaf798 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -38,16 +39,21 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { + DoWork(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index c23fb05aa7..267ab259f5 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -2,10 +2,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Nito.AsyncEx; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -23,12 +26,14 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) : base( + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) : base( distributedCacheOption: distributedCacheOption, cache: cache, cancellationTokenProvider: cancellationTokenProvider, serializer: serializer, - keyNormalizer: keyNormalizer) + keyNormalizer: keyNormalizer, + serviceScopeFactory: serviceScopeFactory) { } @@ -56,6 +61,8 @@ namespace Volo.Abp.Caching protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } + protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected SemaphoreSlim SyncSemaphore { get; } protected DistributedCacheEntryOptions DefaultCacheOptions; @@ -67,7 +74,8 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) { _distributedCacheOption = distributedCacheOption.Value; Cache = cache; @@ -75,6 +83,7 @@ namespace Volo.Abp.Caching Logger = NullLogger>.Instance; Serializer = serializer; KeyNormalizer = keyNormalizer; + ServiceScopeFactory = serviceScopeFactory; SyncSemaphore = new SemaphoreSlim(1, 1); @@ -139,7 +148,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return null; } @@ -181,7 +190,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return null; } @@ -298,7 +307,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -337,7 +346,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -364,7 +373,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -393,7 +402,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -420,7 +429,8 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); + return; } throw; @@ -449,12 +459,24 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } throw; } } + + protected virtual async Task HandleExceptionAsync(Exception ex) + { + Logger.LogException(ex, LogLevel.Warning); + + using (var scope = ServiceScopeFactory.CreateScope()) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); + } + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs index 8140b73b64..59c262e065 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.Cli.ProjectModification { public ILogger Logger { get; set; } - public NpmGlobalPackagesChecker(PackageJsonFileFinder packageJsonFileFinder) + public NpmGlobalPackagesChecker() { Logger = NullLogger.Instance; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs index cb0d727795..cf4befae5b 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Volo.Abp.DependencyInjection { @@ -17,10 +16,11 @@ namespace Volo.Abp.DependencyInjection public static List GetExposedServices(Type type) { return type - .GetCustomAttributes() + .GetCustomAttributes(true) .OfType() .DefaultIfEmpty(DefaultExposeServicesAttribute) .SelectMany(p => p.GetExposedServiceTypes(type)) + .Distinct() .ToList(); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs new file mode 100644 index 0000000000..6f7fbda0af --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs @@ -0,0 +1,32 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotificationContext + { + /// + /// The exception object. + /// + [NotNull] + public Exception Exception { get; } + + public LogLevel LogLevel { get; } + + /// + /// True, if it is handled. + /// + public bool Handled { get; } + + public ExceptionNotificationContext( + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Exception = Check.NotNull(exception, nameof(exception)); + LogLevel = logLevel ?? exception.GetLogLevel(); + Handled = handled; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs new file mode 100644 index 0000000000..b9076c471b --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotifier : IExceptionNotifier, ITransientDependency + { + public ILogger Logger { get; set; } + + protected IEnumerable ExceptionSubscribers { get; } + + public ExceptionNotifier(IEnumerable exceptionSubscribers) + { + ExceptionSubscribers = exceptionSubscribers; + Logger = NullLogger.Instance; + } + + public virtual async Task NotifyAsync([NotNull] ExceptionNotificationContext context) + { + Check.NotNull(context, nameof(context)); + + foreach (var exceptionSubscriber in ExceptionSubscribers) + { + try + { + await exceptionSubscriber.HandleAsync(context); + } + catch (Exception e) + { + Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!"); + Logger.LogException(e, LogLevel.Warning); + } + } + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs new file mode 100644 index 0000000000..493c0cf1f3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public static class ExceptionNotifierExtensions + { + public static Task NotifyAsync( + [NotNull] this IExceptionNotifier exceptionNotifier, + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Check.NotNull(exceptionNotifier, nameof(exceptionNotifier)); + + return exceptionNotifier.NotifyAsync( + new ExceptionNotificationContext( + exception, + logLevel, + handled + ) + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs new file mode 100644 index 0000000000..5f2971c8c4 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + [ExposeServices(typeof(IExceptionSubscriber))] + public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency + { + public abstract Task HandleAsync(ExceptionNotificationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs new file mode 100644 index 0000000000..5646c28e5e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionNotifier + { + Task NotifyAsync([NotNull] ExceptionNotificationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs new file mode 100644 index 0000000000..dc7f336970 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionSubscriber + { + Task HandleAsync([NotNull] ExceptionNotificationContext context); + } +} diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs index d54e7d82d0..9e0b8f04a7 100644 --- a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Options; using Novell.Directory.Ldap; using System.Collections.Generic; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Ldap.Exceptions; using Volo.Abp.Ldap.Modeling; @@ -13,6 +15,7 @@ namespace Volo.Abp.Ldap { private readonly string _searchBase; private readonly AbpLdapOptions _ldapOptions; + private readonly IHybridServiceScopeFactory _hybridServiceScopeFactory; private readonly string[] _attributes = { @@ -21,8 +24,9 @@ namespace Volo.Abp.Ldap "sAMAccountName", "userPrincipalName", "telephoneNumber", "mail" }; - public LdapManager(IOptions ldapSettingsOptions) + public LdapManager(IOptions ldapSettingsOptions, IHybridServiceScopeFactory hybridServiceScopeFactory) { + _hybridServiceScopeFactory = hybridServiceScopeFactory; _ldapOptions = ldapSettingsOptions.Value; _searchBase = _ldapOptions.SearchBase; } @@ -231,8 +235,15 @@ namespace Volo.Abp.Ldap return true; } } - catch (Exception ) + catch (Exception ex) { + using (var scope = _hybridServiceScopeFactory.CreateScope()) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(ex); + } + return false; } } diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs index 1214a82a99..135156f7a8 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.RabbitMQ @@ -17,6 +18,8 @@ namespace Volo.Abp.RabbitMQ protected IConnectionPool ConnectionPool { get; } + protected IExceptionNotifier ExceptionNotifier { get; } + protected AbpTimer Timer { get; } protected ExchangeDeclareConfiguration Exchange { get; private set; } @@ -35,10 +38,12 @@ namespace Volo.Abp.RabbitMQ public RabbitMqMessageConsumer( IConnectionPool connectionPool, - AbpTimer timer) + AbpTimer timer, + IExceptionNotifier exceptionNotifier) { ConnectionPool = connectionPool; Timer = timer; + ExceptionNotifier = exceptionNotifier; Logger = NullLogger.Instance; QueueBindCommands = new ConcurrentQueue(); @@ -114,6 +119,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -180,6 +186,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -197,6 +204,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex); + await ExceptionNotifier.NotifyAsync(ex); } } @@ -214,6 +222,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } diff --git a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs index cae8ac984f..a68d15c7a2 100644 --- a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs +++ b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs @@ -3,11 +3,12 @@ using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.Threading { /// - /// A roboust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. + /// A robust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. /// public class AbpTimer : ITransientDependency { @@ -29,15 +30,23 @@ namespace Volo.Abp.Threading public ILogger Logger { get; set; } + protected IExceptionNotifier ExceptionNotifier { get; } + private readonly Timer _taskTimer; private volatile bool _performingTasks; private volatile bool _isRunning; - public AbpTimer() + public AbpTimer(IExceptionNotifier exceptionNotifier) { + ExceptionNotifier = exceptionNotifier; Logger = NullLogger.Instance; - _taskTimer = new Timer(TimerCallBack, null, Timeout.Infinite, Timeout.Infinite); + _taskTimer = new Timer( + TimerCallBack, + null, + Timeout.Infinite, + Timeout.Infinite + ); } public void Start(CancellationToken cancellationToken = default) @@ -89,9 +98,10 @@ namespace Volo.Abp.Threading { Elapsed.InvokeSafely(this, new EventArgs()); } - catch + catch(Exception ex) { - + Logger.LogException(ex); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex)); } finally { diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs index 5243307116..0de46fbe55 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs @@ -1,6 +1,10 @@ using System.Net; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NSubstitute; using Shouldly; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Xunit; @@ -8,12 +12,27 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { public class ExceptionTestController_Tests : AspNetCoreMvcTestBase { + private IExceptionSubscriber _fakeExceptionSubscriber; + + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + base.ConfigureServices(context, services); + + _fakeExceptionSubscriber = Substitute.For(); + + services.AddSingleton(_fakeExceptionSubscriber); + } + [Fact] public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value() { var result = await GetResponseAsObjectAsync("/api/exception-test/UserFriendlyException1", HttpStatusCode.Forbidden); result.Error.ShouldNotBeNull(); result.Error.Message.ShouldBe("This is a sample exception!"); + + _fakeExceptionSubscriber + .Received() + .HandleAsync(Arg.Any()); } [Fact] @@ -24,6 +43,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling "/api/exception-test/UserFriendlyException2" ) ); + + _fakeExceptionSubscriber + .DidNotReceive() + .HandleAsync(Arg.Any()); } } } diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml index b4ee800394..fe5207cb52 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml @@ -43,8 +43,9 @@

@Html.Raw(GetShortContent(post.Content)) - Continue Reading

+ Continue Reading → + @if (post.Writer != null) { @@ -113,7 +114,7 @@ } @L["WiewsWithCount", post.ReadCount] | - @L["CommentWithCount", post.CommentCount] + @L["CommentWithCount", post.CommentCount] @@ -157,11 +158,12 @@

@Html.Raw(GetShortContent(post.Content)) - Continue Reading

+ Continue Reading → +