Merge remote-tracking branch 'abpframework/dev' into docs

pull/3340/head
liangshiwei 5 years ago
commit 9a4b1c99e1

@ -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<AbpExceptionHttpStatusCodeOptions>(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:

@ -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

@ -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)

@ -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<null> = {
method: 'GET',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, FooResponse>(request);
}
```
The `request` method always returns an `Observable<T>`. 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<T>` 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<Foo> = {
method: 'POST',
url: '/api/some/path/to/foo',
body
};
return this.rest.request<Foo, FooResponse>(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<T>` 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<null> = {
method: 'DELETE',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, void>(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<Foo> = {
method: 'PUT',
url: '/' + id,
body
};
return this.rest.request<Foo, void>(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<null> = {
method: 'GET',
url: '/api/some/path/that/sends/some-custom-header',
};
return this.rest.request<null, HttpResponse<any>>(
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)

@ -133,4 +133,8 @@ Localization resources are stored in the `localization` property of `ConfigState
## See Also
* [Localization in ASP.NET Core](../../Localization.md)
* [Localization in ASP.NET Core](../../Localization.md)
## What's Next?
* [Permission Management](./Permission-Management.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<Item>) {}
}
```
> 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
<!-- template of DemoComponent -->
<div *ngFor="let item of list; trackBy: track.by('id')">{%{{{ item.name }}}%}</div>
```
`by` is exported as a stand-alone function and is named `trackBy`.
```js
import { trackBy } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackById"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackById = trackBy<Item>('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
<!-- template of DemoComponent -->
<div
*ngFor="let item of list; trackBy: track.byDeep('tenant', 'account', 'id')"
>
{%{{{ item.tenant.name }}}%}
</div>
```
`byDeep` is exported as a stand-alone function and is named `trackByDeep`.
```js
import { trackByDeep } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackByTenantAccountId"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id');
}
```

@ -0,0 +1,3 @@
# ABP Datatables.Net Integration for ASP.NET Core UI
TODO

@ -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
<abp-button button-type="Primary" abp-collapse-id="collapseExample" text="Button with data-target" />
<a abp-button="Primary" abp-collapse-id="collapseExample"> Link with href </a>
<abp-collapse-body id="collapseExample">
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.
</abp-collapse-body>
````
## 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
<a abp-button="Primary" abp-collapse-id="FirstCollapseExample"> Toggle first element </a>
<abp-button button-type="Primary" abp-collapse-id="SecondCollapseExample" text="Toggle second element" />
<abp-button button-type="Primary" abp-collapse-id="FirstCollapseExample SecondCollapseExample" text="Toggle both elements" />
<abp-row class="mt-3">
<abp-column size-sm="_6">
<abp-collapse-body id="FirstCollapseExample" multi="true">
Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit
</abp-collapse-body>
</abp-column>
<abp-column size-sm="_6">
<abp-collapse-body id="SecondCollapseExample" multi="true">
Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et.
</abp-collapse-body>
</abp-column>
</abp-row>
````
## Accordion example
`abp-accordion` is the main container for the accordion items.
Basic usage:
````xml
<abp-accordion>
<abp-accordion-item title="Collapsible Group Item #1">
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.
</abp-accordion-item>
<abp-accordion-item title="Collapsible Group Item #2">
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.
</abp-accordion-item>
<abp-accordion-item title="Collapsible Group Item #3">
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.
</abp-accordion-item>
</abp-accordion>
````
## 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.

@ -0,0 +1,97 @@
# Dropdowns
## Introduction
`abp-dropdown` is the main container for dropdown content.
Basic usage:
````xml
<abp-dropdown>
<abp-dropdown-button text="Dropdown button" />
<abp-dropdown-menu>
<abp-dropdown-item href="#">Action</abp-dropdown-item>
<abp-dropdown-item href="#">Another action</abp-dropdown-item>
<abp-dropdown-item href="#">Something else here</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````
## 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
<abp-dropdown>
<abp-dropdown-button button-type="Secondary" text="Dropdown"/>
<abp-dropdown-menu>
<abp-dropdown-header>Dropdown Header</abp-dropdown-header>
<abp-dropdown-item href="#">Action</abp-dropdown-item>
<abp-dropdown-item active="true" href="#">Active action</abp-dropdown-item>
<abp-dropdown-item disabled="true" href="#">Disabled action</abp-dropdown-item>
<abp-dropdown-divider/>
<abp-dropdown-item-text>Dropdown Item Text</abp-dropdown-item-text>
<abp-dropdown-item href="#">Something else here</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````
## 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
<abp-dropdown >
<abp-dropdown-button button-type="Secondary" text="Dropdown With Form"/>
<abp-dropdown-menu>
<form class="px-4 py-3">
<abp-input asp-for="EmailAddress"></abp-input>
<abp-input asp-for="Password"></abp-input>
<abp-input asp-for="RememberMe"></abp-input>
<abp-button button-type="Primary" text="Sign In" type="submit" />
</form>
<abp-dropdown-divider></abp-dropdown-divider>
<abp-dropdown-item href="#">New around here? Sign up</abp-dropdown-item>
<abp-dropdown-item href="#">Forgot password?</abp-dropdown-item>
</abp-dropdown-menu>
</abp-dropdown>
````

@ -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.

@ -0,0 +1,78 @@
# List Groups
## Introduction
`abp-list-group` is the main container for list group content.
Basic usage:
````xml
<abp-list-group>
<abp-list-group-item>Cras justo odio</abp-list-group-item>
<abp-list-group-item>Dapibus ac facilisis in</abp-list-group-item>
<abp-list-group-item>Morbi leo risus</abp-list-group-item>
<abp-list-group-item>Vestibulum at eros</abp-list-group-item>
</abp-list-group>
````
## 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
<abp-list-group>
<abp-list-group-item>Cras justo odio <span abp-badge-pill="Primary">14</span></abp-list-group-item>
<abp-list-group-item>Dapibus ac facilisis in <span abp-badge-pill="Primary">2</span></abp-list-group-item>
<abp-list-group-item>Morbi leo risus <span abp-badge-pill="Primary">1</span></abp-list-group-item>
</abp-list-group>
````

@ -0,0 +1,81 @@
# Modals
## Introduction
`abp-modal` is a main element to create a modal.
Basic usage:
````xml
<abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
<abp-modal centered="true" size="Large" id="myModal">
<abp-modal-header title="Modal title"></abp-modal-header>
<abp-modal-body>
Woohoo, you're reading this text in a modal!
</abp-modal-body>
<abp-modal-footer buttons="Close"></abp-modal-footer>
</abp-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
<abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
<abp-modal centered="true" size="Large" id="myModal" static="true">
<abp-modal-header title="Modal title"></abp-modal-header>
<abp-modal-body>
Woohoo, you're reading this text in a modal!
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Save|AbpModalButtons.Close)" button-alignment="Between"></abp-modal-footer>
</abp-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`

@ -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
<abp-paginator model="Model.PagerModel" show-info="true"></abp-paginator>
````
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`

@ -0,0 +1,70 @@
# Progress Bars
## Introduction
`abp-progress-bar` is the abp tag for progress bar status.
Basic usage:
````xml
<abp-progress-bar value="70" />
<abp-progress-bar type="Warning" value="25"> %25 </abp-progress-bar>
<abp-progress-bar type="Success" value="40" strip="true"/>
<abp-progress-bar type="Dark" value="10" min-value="5" max-value="15" strip="true"> %50 </abp-progress-bar>
<abp-progress-group>
<abp-progress-part type="Success" value="25"/>
<abp-progress-part type="Danger" value="10" strip="true"> %10 </abp-progress-part>
<abp-progress-part type="Primary" value="50" animation="true" strip="true" />
</abp-progress-group>
````
## 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`

@ -0,0 +1,35 @@
# Tooltips
## Introduction
`abp-tooltip` is the abp tag for tooltips.
Basic usage:
````xml
<abp-button abp-tooltip="Tooltip">
Tooltip Default
</abp-button>
<abp-button abp-tooltip-top="Tooltip">
Tooltip on top
</abp-button>
<abp-button abp-tooltip-right="Tooltip">
Tooltip on right
</abp-button>
<abp-button abp-tooltip-bottom="Tooltip">
Tooltip on bottom
</abp-button>
<abp-button disabled="true" abp-tooltip="Tooltip">
Disabled button Tooltip
</abp-button>
````
## Demo
See the [tooltips demo page](https://bootstrap-taghelpers.abp.io/Components/Tooltips) to see it in action.

@ -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"
}
]
}

@ -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<AbpUiResource> _localizer;
private readonly AbpErrorPageOptions _abpErrorPageOptions;
private readonly IExceptionNotifier _exceptionNotifier;
public ErrorController(
IExceptionToErrorInfoConverter exceptionToErrorInfoConverter,
IHttpExceptionStatusCodeFinder httpExceptionStatusCodeFinder,
IOptions<AbpErrorPageOptions> abpErrorPageOptions,
IStringLocalizer<AbpUiResource> localizer)
IStringLocalizer<AbpUiResource> localizer,
IExceptionNotifier exceptionNotifier)
{
_errorInfoConverter = exceptionToErrorInfoConverter;
_statusCodeFinder = httpExceptionStatusCodeFinder;
_localizer = localizer;
_exceptionNotifier = exceptionNotifier;
_abpErrorPageOptions = abpErrorPageOptions.Value;
}
public IActionResult Index(int httpStatusCode)
public async Task<IActionResult> Index(int httpStatusCode)
{
var exHandlerFeature = HttpContext.Features.Get<IExceptionHandlerFeature>();
@ -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)

@ -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<AbpExceptionFilter> Logger { get; set; }
@ -32,14 +35,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
Logger = NullLogger<AbpExceptionFilter>.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<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(context.Exception)
);
context.Exception = null; //Handled!
}
}

@ -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<IExceptionNotifier>()
.NotifyAsync(
new ExceptionNotificationContext(exception)
);
}
private Task ClearCacheHeaders(object state)

@ -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<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
throw new BackgroundJobExecutionException("A background job execution is failed. See inner exception for details.", ex)
{
JobType = context.JobType.AssemblyQualifiedName,

@ -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));

@ -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<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex))
);
Logger.LogException(ex);
}
}
}

@ -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<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
Logger.LogException(ex);
}
}
}

@ -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<DistributedCache<TCacheItem, TCacheKey>>.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<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
}
}
}
}

@ -9,7 +9,7 @@ namespace Volo.Abp.Cli.ProjectModification
{
public ILogger<NpmGlobalPackagesChecker> Logger { get; set; }
public NpmGlobalPackagesChecker(PackageJsonFileFinder packageJsonFileFinder)
public NpmGlobalPackagesChecker()
{
Logger = NullLogger<NpmGlobalPackagesChecker>.Instance;
}

@ -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<Type> GetExposedServices(Type type)
{
return type
.GetCustomAttributes()
.GetCustomAttributes(true)
.OfType<IExposedServiceTypesProvider>()
.DefaultIfEmpty(DefaultExposeServicesAttribute)
.SelectMany(p => p.GetExposedServiceTypes(type))
.Distinct()
.ToList();
}
}

@ -0,0 +1,32 @@
using System;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.ExceptionHandling
{
public class ExceptionNotificationContext
{
/// <summary>
/// The exception object.
/// </summary>
[NotNull]
public Exception Exception { get; }
public LogLevel LogLevel { get; }
/// <summary>
/// True, if it is handled.
/// </summary>
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;
}
}
}

@ -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<ExceptionNotifier> Logger { get; set; }
protected IEnumerable<IExceptionSubscriber> ExceptionSubscribers { get; }
public ExceptionNotifier(IEnumerable<IExceptionSubscriber> exceptionSubscribers)
{
ExceptionSubscribers = exceptionSubscribers;
Logger = NullLogger<ExceptionNotifier>.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);
}
}
}
}
}

@ -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
)
);
}
}
}

@ -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);
}
}

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.ExceptionHandling
{
public interface IExceptionNotifier
{
Task NotifyAsync([NotNull] ExceptionNotificationContext context);
}
}

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.ExceptionHandling
{
public interface IExceptionSubscriber
{
Task HandleAsync([NotNull] ExceptionNotificationContext context);
}
}

@ -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<AbpLdapOptions> ldapSettingsOptions)
public LdapManager(IOptions<AbpLdapOptions> 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<IExceptionNotifier>()
.NotifyAsync(ex);
}
return false;
}
}

@ -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<RabbitMqMessageConsumer>.Instance;
QueueBindCommands = new ConcurrentQueue<QueueBindCommand>();
@ -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));
}
}

@ -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
{
/// <summary>
/// A roboust timer implementation that ensures no overlapping occurs. It waits exactly specified <see cref="Period"/> between ticks.
/// A robust timer implementation that ensures no overlapping occurs. It waits exactly specified <see cref="Period"/> between ticks.
/// </summary>
public class AbpTimer : ITransientDependency
{
@ -29,15 +30,23 @@ namespace Volo.Abp.Threading
public ILogger<AbpTimer> 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<AbpTimer>.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
{

@ -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<IExceptionSubscriber>();
services.AddSingleton(_fakeExceptionSubscriber);
}
[Fact]
public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value()
{
var result = await GetResponseAsObjectAsync<RemoteServiceErrorResponse>("/api/exception-test/UserFriendlyException1", HttpStatusCode.Forbidden);
result.Error.ShouldNotBeNull();
result.Error.Message.ShouldBe("This is a sample exception!");
_fakeExceptionSubscriber
.Received()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
}
[Fact]
@ -24,6 +43,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling
"/api/exception-test/UserFriendlyException2"
)
);
_fakeExceptionSubscriber
.DidNotReceive()
.HandleAsync(Arg.Any<ExceptionNotificationContext>());
}
}
}

@ -43,8 +43,9 @@
</h2>
<p class="article-sum">
@Html.Raw(GetShortContent(post.Content))
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName">Continue Reading</a>
</p>
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName" class="read-more-btn">Continue Reading &#8594;</a>
</div>
@if (post.Writer != null)
{
@ -113,7 +114,7 @@
}
<i class="fa fa-eye"></i> @L["WiewsWithCount", post.ReadCount]
<span class="vs-seperator">|</span>
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]
<i class="fa fa-comment"></i> @L["CommentWithCount", post.CommentCount]
</div>
</div>
</div>
@ -157,11 +158,12 @@
</h3>
<p>
@Html.Raw(GetShortContent(post.Content))
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName">Continue Reading</a>
</p>
<a asp-page="./Detail" asp-route-postUrl="@post.Url" asp-route-blogShortName="@Model.BlogShortName" class="read-more-btn">Continue Reading &#8594;</a>
<div class="article-owner">
<div class="article-infos">
<div class="user-card">
<div class="user-card">
<div class="row">
<div class="col-auto pr-1">
<img gravatar-email="@post.Writer.Email" default-image="Identicon" class="article-avatar" />
@ -198,14 +200,11 @@
<div class="vs-blog-title">
<h2>@L["PopularTags"]</h2>
</div>
<div class="list-group small-list popular-tags">
<div class="tags">
@foreach (var popularTag in Model.PopularTags)
{
<div class="list-group-item">
<a asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <span>(@popularTag.UsageCount @L["Posts"])</span></a>
</div>
}
{
<a class="tag" asp-page="/Blogs/Posts/Index" asp-route-blogShortName="@Model.BlogShortName" asp-route-tagName="@popularTag.Name">@popularTag.Name <small>(@popularTag.UsageCount)</small></a>
}
</div>
</div>
}

@ -71,7 +71,7 @@ input, select, textarea, .form-control, .btn {
& + .list-group-item {
border-top: 1px solid #f5f5f5;
padding: 45px 0 30px;
padding: 35px 0 30px;
}
h3 {
@ -104,5 +104,9 @@ input, select, textarea, .form-control, .btn {
}
.font-125 {
font-size: 1.125em;
}
}
pre {
background: #eee;
padding: 25px 30px;
margin: 1em 0 2em 0;
}

@ -48,7 +48,15 @@
}
}
.read-more-btn {
display: inline-block;
font-size: .9em;
margin: 0 0 1em 0;
background: #eee;
color: blue;
padding: .25em 1em;
border-radius: 20px;
}
.comment-area {
background: #f5f5f5;
margin: 1.5rem 0;

@ -56,7 +56,7 @@
background: none; }
div.vs-blog .list-group .list-group-item + .list-group-item {
border-top: 1px solid #f5f5f5;
padding: 45px 0 30px; }
padding: 35px 0 30px; }
div.vs-blog .list-group .list-group-item h3 {
margin-top: 0; }
div.vs-blog .list-group.small-list .list-group-item {
@ -73,6 +73,10 @@
font-size: 1em; }
div.vs-blog .font-125 {
font-size: 1.125em; }
div.vs-blog pre {
background: #eee;
padding: 25px 30px;
margin: 1em 0 2em 0; }
div.vs-blog .vs-blog-title {
padding-bottom: 15px;
margin-bottom: 25px;
@ -236,6 +240,14 @@
font-size: .95em; }
div.vs-blog .media .media {
font-size: .95em; }
div.vs-blog .read-more-btn {
display: inline-block;
font-size: .9em;
margin: 0 0 1em 0;
background: #eee;
color: blue;
padding: .25em 1em;
border-radius: 20px; }
div.vs-blog .comment-area {
background: #f5f5f5;
margin: 1.5rem 0;

File diff suppressed because one or more lines are too long

@ -119,16 +119,17 @@
};
var setQueryString = function () {
clearQueryString();
var uri = window.location.href.toString();
var comboboxes = $(".doc-section-combobox");
if (comboboxes.length < 1) {
return;
}
var hash = document.location.hash;
clearQueryString();
var uri = window.location.href.toString();
var new_uri = uri + "?";
for (var i = 0; i < comboboxes.length; i++) {
@ -142,7 +143,7 @@
}
}
window.history.replaceState({}, document.title, new_uri);
window.history.replaceState({}, document.title, new_uri + hash);
};
var getTenYearsLater = function () {

@ -12,6 +12,18 @@ namespace Volo.Abp.IdentityServer
)]
public class AbpIdentityServerTestBaseModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpIdentityServerBuilderOptions>(options =>
{
options.AddDeveloperSigningCredential = false;
});
PreConfigure<IIdentityServerBuilder>(identityServerBuilder =>
{
identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString());
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();

@ -44,6 +44,7 @@ namespace Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants
public string AdminEmailAddress { get; set; }
[Required]
[DataType(DataType.Password)]
[MaxLength(128)]
public string AdminPassword { get; set; }
}

@ -45,4 +45,5 @@ testem.log
.DS_Store
Thumbs.db
!**/[Pp]ackages/*
!**/[Pp]ackages/*
*.internal.*

@ -6,6 +6,7 @@
"typescript.tsdk": "../node_modules/typescript/lib",
"workbench.colorCustomizations": {
"activityBar.background": "#258ecd",
"activityBar.activeBackground": "#258ecd",
"activityBar.activeBorder": "#f0aed7",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",

@ -485,6 +485,14 @@
"maximumError": "10kb"
}
]
},
"internal": {
"fileReplacements": [
{
"replace": "apps/dev-app/src/environments/environment.ts",
"with": "apps/dev-app/src/environments/environment.internal.ts"
}
]
}
}
},
@ -496,6 +504,9 @@
"configurations": {
"production": {
"browserTarget": "dev-app:build:production"
},
"internal": {
"browserTarget": "dev-app:build:internal"
}
}
},

@ -6,9 +6,9 @@
},
"scripts": {
"ng": "ng",
"abpng": "abpng",
"symlink": "symlink",
"start": "ng serve dev-app",
"start:internal": "ng serve dev-app --configuration=internal",
"test": "ng test --watchAll --runInBand",
"commit": "git-cz",
"lint": "ng lint",
@ -21,19 +21,19 @@
"generate:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"devDependencies": {
"@abp/ng.account": "~2.2.0",
"@abp/ng.account.config": "~2.2.0",
"@abp/ng.core": "~2.2.0",
"@abp/ng.feature-management": "~2.2.0",
"@abp/ng.identity": "~2.2.0",
"@abp/ng.identity.config": "~2.2.0",
"@abp/ng.permission-management": "~2.2.0",
"@abp/ng.setting-management": "~2.2.0",
"@abp/ng.setting-management.config": "~2.2.0",
"@abp/ng.tenant-management": "~2.2.0",
"@abp/ng.tenant-management.config": "~2.2.0",
"@abp/ng.theme.basic": "~2.2.0",
"@abp/ng.theme.shared": "~2.2.0",
"@abp/ng.account": "~2.3.0",
"@abp/ng.account.config": "~2.3.0",
"@abp/ng.core": "^2.3.0",
"@abp/ng.feature-management": "^2.3.0",
"@abp/ng.identity": "~2.3.0",
"@abp/ng.identity.config": "~2.3.0",
"@abp/ng.permission-management": "^2.3.0",
"@abp/ng.setting-management": "~2.3.0",
"@abp/ng.setting-management.config": "~2.3.0",
"@abp/ng.tenant-management": "~2.3.0",
"@abp/ng.tenant-management.config": "~2.3.0",
"@abp/ng.theme.basic": "~2.3.0",
"@abp/ng.theme.shared": "^2.3.0",
"@angular-builders/jest": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.21",
"@angular-devkit/build-ng-packagr": "~0.803.21",
@ -83,6 +83,7 @@
"snq": "^1.0.3",
"symlink-manager": "^1.4.2",
"ts-node": "~7.0.0",
"ts-toolbelt": "^6.3.6",
"tsickle": "^0.37.0",
"tslint": "~5.20.0",
"typescript": "~3.5.3",

@ -1,18 +1,17 @@
import { Injectable, TrackByFunction } from '@angular/core';
import { O } from 'ts-toolbelt';
export const trackBy = <T = any>(key: keyof T): TrackByFunction<T> => (_, item) => item[key];
export const trackByDeep = <T = any>(
...keys: T extends object ? O.Paths<T> : never
): TrackByFunction<T> => (_, item) => keys.reduce((acc, key) => acc[key], item);
@Injectable({
providedIn: 'root',
})
export class TrackByService<ItemType = any> {
by<T = ItemType>(key: keyof T): TrackByFunction<T> {
return ({}, item) => item[key];
}
byDeep<T = ItemType>(...keys: (string | number)[]): TrackByFunction<T> {
return ({}, item) => keys.reduce((acc, key) => acc[key], item);
}
by = trackBy;
bySelf<T = ItemType>(): TrackByFunction<T> {
return ({}, item) => item;
}
byDeep = trackByDeep;
}

@ -262,7 +262,8 @@ export class ConfigState {
route.url = `/${route.path}`;
}
route.order = route.order || route.order === 0 ? route.order : parent.children.length;
route.children = route.children || [];
route.order = route.order || route.order === 0 ? route.order : (parent.children || []).length;
parent.children = [...(parent.children || []), route].sort((a, b) => a.order - b.order);
flattedRoutes[index] = parent;

@ -375,6 +375,7 @@ describe('ConfigState', () => {
describe('#AddRoute', () => {
const newRoute = {
name: 'My new page',
children: [],
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,

@ -11,23 +11,11 @@ describe('TrackByService', () => {
describe('#byDeep', () => {
it('should return a function which tracks a deeply-nested property', () => {
expect(
service.byDeep(
'a',
'b',
'c',
1,
'x',
)(284, {
a: { b: { c: [{ x: 1035 }, { x: 1036 }, { x: 1037 }] } },
}),
).toBe(1036);
});
});
const obj = {
a: { b: { c: { x: 1036 } } },
};
describe('#bySelf', () => {
it('should return a function which tracks the item', () => {
expect(service.bySelf()(284, 'X')).toBe('X');
expect(service.byDeep<typeof obj>('a', 'b', 'c', 'x')(284, obj)).toBe(1036);
});
});
});

@ -13,11 +13,15 @@ const updateAndInstall = async () => {
`../packages/${project}/package.json`,
);
const isPackageExistOnNPM = !(
execa.sync('npm', ['search', name]).stdout.indexOf('No matches found for') > -1
);
packageJson.devDependencies = {
...packageJson.devDependencies,
...dependencies,
...peerDependencies,
...{ [name]: `~${version}` },
...(isPackageExistOnNPM && { [name]: `~${version}` }),
};
packageJson.devDependencies = Object.keys(packageJson.devDependencies)

@ -2,26 +2,26 @@
# yarn lockfile v1
"@abp/ng.account.config@^2.2.0", "@abp/ng.account.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account.config/-/ng.account.config-2.2.0.tgz#420396fc55eadd9a5c179e87075a51437ebc964c"
integrity sha512-5BpxFnXCeCDR+m3qGMKn8rSMGwBb4mkwtejVAJXCVYoMXL8x2J9uRgDf9fkdudqpls+BIgB8BX1tRyJiY+Bg8Q==
"@abp/ng.account.config@^2.3.0", "@abp/ng.account.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account.config/-/ng.account.config-2.3.0.tgz#28d65e2eb889d8b44fc726edbed7509214e8034a"
integrity sha512-Vg4+8PvGfgUC+pFtPIS53ZekJM+O4JZ8wGPGaZ/ySLpk2oSfzC/5RFS2rKq3cmySeRCJunmQinCAlBCm5zir8A==
dependencies:
tslib "^1.9.0"
"@abp/ng.account@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account/-/ng.account-2.2.0.tgz#3a200a46f83c36ae89a6724a6a2226a5f846641b"
integrity sha512-OwTOxXDI3BJ1MrlR6WtvDmNIkqi1OCWlq7MvUhuFPVmIIUI3OjxHNASk1NfWMSlu6amXDxuFEey4ItrMKnAJog==
"@abp/ng.account@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.account/-/ng.account-2.3.0.tgz#9ca2a564c177c43b53f69f141405914baefbc7b2"
integrity sha512-kJCek8woGGEC1WTgo75Qr2ucyD5VV1nqqnkw8UMJ96/pqASpBJHczGTL8R4ug961i4vexKDnMbiv0SmfMlBDig==
dependencies:
"@abp/ng.account.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.account.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.core@^2.2.0", "@abp/ng.core@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-2.2.0.tgz#a553e845f7bc43838704eb15d8684869dfcb053b"
integrity sha512-HtyHJYPY6kKqySt/afgFT5j6yaN7Bx4MMvIGWHqFqZsQChWceagLk5SBFTOjCE1FgsewX2a7OT452bKjgsWsrg==
"@abp/ng.core@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-2.3.0.tgz#96bad951da07e589f11a0345171e1a142463671e"
integrity sha512-6SsgcRJQWjXvyEZ7VO2ekOoVmKkFB17vJgTvQLiyB+j2t9guAo3LPDcMGGKNum/oInkiYczf8MY7sculrElyKQ==
dependencies:
"@angular/localize" "~9.0.2"
"@ngxs/router-plugin" "^3.6.2"
@ -33,86 +33,86 @@
snq "^1.0.3"
tslib "^1.9.0"
"@abp/ng.feature-management@^2.2.0", "@abp/ng.feature-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-2.2.0.tgz#04f959ddb62a0abd99a84fb4e6ec935a796418d4"
integrity sha512-Cw0GRi+6LX5oKDwEvJJyUoh7M4hvaUE5TsuP1E3Hicg/1mMSyUDXVrLBOvAeWWolyfFTpFKLppAvoEEMpUt2eg==
"@abp/ng.feature-management@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-2.3.0.tgz#91172fa9f308b3a792a2bf1e762e24560556f412"
integrity sha512-ShytiV1SC3PwP4Hs8Ss3bk2UAZBXteHKWcrIhAvqtWsOQ2aql6e7t+FutRA3wtUuRdX6PTrMRXbMRvhCwGZUKQ==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.identity.config@^2.2.0", "@abp/ng.identity.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity.config/-/ng.identity.config-2.2.0.tgz#ac050ed632624c490d8957a18606622637d1d6f3"
integrity sha512-sHRG0iRFrGtF2pNnKcjBaeupg39V8easzUFPU42/SVPi0XyAumrOZRbKkN2CBl0WYedqARTZxE5/nejRMXX4Fg==
"@abp/ng.identity.config@^2.3.0", "@abp/ng.identity.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity.config/-/ng.identity.config-2.3.0.tgz#e9f4904d60f94cc01b3254e0221d7cdb2274aa3b"
integrity sha512-bqCaPHCwaHUfAfNfFskGTJHGvv+hPK9Tmm7PouVa884AmeQs2j0Gwl3o93YHK2VwXENV09qb01feXapSVPsmTw==
dependencies:
tslib "^1.9.0"
"@abp/ng.identity@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-2.2.0.tgz#1cc7ffa4e2aae462d8ce2399f94cf5ad047fefae"
integrity sha512-G/hrMg/PaN0tA871D52AuKlUhsLWfWSblK0XqQiRmMp/ozsEYSvAV91n/pEScm7qx0RF01K0J5K5V8Cjb4LTyA==
"@abp/ng.identity@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-2.3.0.tgz#c973f12f5490b1d29c80f510540b18343f998b8d"
integrity sha512-vBLLxCax7MoHilakpW9XMRFGUXcdMM0syiz0PtkgKmYADIQIQ9AzcfwslK2D6wwy447s6NIoGnThtfbshoRtLw==
dependencies:
"@abp/ng.identity.config" "^2.2.0"
"@abp/ng.permission-management" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.identity.config" "^2.3.0"
"@abp/ng.permission-management" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.permission-management@^2.2.0", "@abp/ng.permission-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-2.2.0.tgz#56bece037aa094400f8d5585db0bfd5718437ae0"
integrity sha512-OeUZzZV+2TTWOhmpwFTvar9/4IpKz5EhI/6uabu3pOtIsU2Ms2OBbjwVUSKbhLT7e0+z5MM1nPCDoXIlYv22wA==
"@abp/ng.permission-management@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-2.3.0.tgz#16599a2e1583c9d6769edb1dc9b78a45b75b7402"
integrity sha512-vJSfcmXCXpBHMjeRb/0QoKlAcvpCQ/qS2quHFUr23nriXIeNxgEdCVHkTBZU8FMxfyTfwwrFRzF2oxn33gubbg==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.setting-management.config@^2.2.0", "@abp/ng.setting-management.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management.config/-/ng.setting-management.config-2.2.0.tgz#31cf94a785fc2d5c3f1bcfe2c79afc0f1ab419ee"
integrity sha512-vZhBvKFZ6puWwujkzEEhqyQiMKdTmAeJjSxlUPRyIuwowrZhPnUk1r0ghyOLTC5fC1TDaUXF1mZ2UsBFMDvuiw==
"@abp/ng.setting-management.config@^2.3.0", "@abp/ng.setting-management.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management.config/-/ng.setting-management.config-2.3.0.tgz#80b9ebc659c34be4c4c45cc6f2fe2b593f491aab"
integrity sha512-nj6Hl8hlzrGJFJZo4d9DWQtf1PCSFnXup/3ajqMOCPPE+oBItY3aNY05jDyGgdr2wEdyWo/u9Invy3Jsvq8itQ==
dependencies:
tslib "^1.9.0"
"@abp/ng.setting-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-2.2.0.tgz#e1c976bf69bbfaa452489b8968b8669c3fba7710"
integrity sha512-LL6CUi0qpS0+9kPz/T0n1U+Kpu9gwI49+d/+xVDG+lIziGqukgwgoUwlEv+Lk0ak+RC5noLurvvJaaKd3l3mPw==
"@abp/ng.setting-management@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-2.3.0.tgz#478c603f67416df763228bc958b6ea05e7d13dd7"
integrity sha512-xJk09NdpXeg3/KezvKl+h0B4T08Hy1SofOATQpVTrE4TIsFxyXKmasMcSY4i/3+wwE1umH+6TZJwPE+pXTu9Ng==
dependencies:
"@abp/ng.setting-management.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.setting-management.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.tenant-management.config@^2.2.0", "@abp/ng.tenant-management.config@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management.config/-/ng.tenant-management.config-2.2.0.tgz#16d87f31ec069e1f3a8351ef3be9a05a3002ed26"
integrity sha512-lfW9lGERn9PBIRseJajQ0GSxo1+wfRxO7Ic/lSSPxhUbsmwg6afquYTcGiU0d+4QQOBY47ga3n0IVrNqWq1pmw==
"@abp/ng.tenant-management.config@^2.3.0", "@abp/ng.tenant-management.config@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management.config/-/ng.tenant-management.config-2.3.0.tgz#8501a3a53f1abac8a65a87782752afb2ba648bb1"
integrity sha512-gGqg7rZd5X37z9glYF2lSiFpJ3Lyi1NdqHnaxdCTui+3/weMo/5RKlf+ilUAPqR5YAMVSLi4mBYYsuShWEBBIQ==
dependencies:
tslib "^1.9.0"
"@abp/ng.tenant-management@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-2.2.0.tgz#0ce498eaf9f65ef0255fc23949a47f8ae36cf5c8"
integrity sha512-v9Y5F9fm2EXYteCWKI8QODN4ETmSdh6K7gC5Y3+/N+QaUAod8JxFNX0EIXzFGnSLbiZ0O1xA/TRJruQTv1m3SA==
"@abp/ng.tenant-management@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-2.3.0.tgz#5ec092ad9c597d4aa9f2f849fc12e955d2d24696"
integrity sha512-LyJaXuzgZr2tfFPknuGz1spOzfxfaEhPuQ6zHBOhVfQ6KtMBeagsQQoEiLJMtcygR0MRox0kzmUiyTKm4H5DoA==
dependencies:
"@abp/ng.feature-management" "^2.2.0"
"@abp/ng.tenant-management.config" "^2.2.0"
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.feature-management" "^2.3.0"
"@abp/ng.tenant-management.config" "^2.3.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.theme.basic@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-2.2.0.tgz#7a086a27daa3bec16962dd62dfb2f39a1538e82f"
integrity sha512-fpdDjjhEQZtaZvFkVi5uhqZoYyrxCWJeQgGFvLS36TqYqvJVyoMeJBlVw0CgbY3F+u5V/GmZSVxvZAAJDpUYhg==
"@abp/ng.theme.basic@~2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-2.3.0.tgz#e5857864ae4c3274a57cc785b06cccd0ff3515a8"
integrity sha512-LuAlKqmqEFUUwI/ruB6aO1rhfsCD19Pt7PCE3M+vr/KvlYAKb89K1U8nphIfMV8PCz9IU07BwvTn/T2qIt39HQ==
dependencies:
"@abp/ng.theme.shared" "^2.2.0"
"@abp/ng.theme.shared" "^2.3.0"
tslib "^1.9.0"
"@abp/ng.theme.shared@^2.2.0", "@abp/ng.theme.shared@~2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-2.2.0.tgz#438f77498df3e2f25a1ecf9adb77a9ee5a71d2c5"
integrity sha512-w7TnDbdHpOFcT12wt/9nZDH9PkyZdTP6W+tJIGeH6zOgWC8V4MDX8Ulc9e/ZvQ8u0qRLOEmk3aE5odFQUHSlJw==
"@abp/ng.theme.shared@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-2.3.0.tgz#5b13b8e170fb0c2a4afca34434bd455ee8dfb9d6"
integrity sha512-keYnD17K8QkdSLqBbsATfeh7KwKNoUj/XsHZO8hawE3OfRuy4qYY9xghGFZUUqdIE5kF6gKlLKj4ZTZmuCvOmQ==
dependencies:
"@abp/ng.core" "^2.2.0"
"@abp/ng.core" "^2.3.0"
"@fortawesome/fontawesome-free" "^5.12.1"
"@ng-bootstrap/ng-bootstrap" "^5.3.0"
"@ngx-validate/core" "^0.0.7"
@ -11685,6 +11685,11 @@ ts-node@~7.0.0:
source-map-support "^0.5.6"
yn "^2.0.0"
ts-toolbelt@^6.3.6:
version "6.3.6"
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.3.6.tgz#2bde29106c013ed520c32f30e1248daf8fd4f5f9"
integrity sha512-eVzym+LyQodOCfyVyQDQ6FGYbO2Xf9Nc4dGLRKlKSUpAs+8qQWHG+grDiA3ciEuNPNZ0qJnNIYkdqBW1rCWuUA==
tsickle@^0.37.0:
version "0.37.1"
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.37.1.tgz#2f8a87c1b15766e866457bd06fb6c0e0d84eed09"

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -38,15 +39,14 @@ namespace MyCompanyName.MyProjectName.Data
await MigrateHostDatabaseAsync();
var tenants = await _tenantRepository.GetListAsync(includeDetails: true);
var i = 0;
var tenants = await _tenantRepository.GetListAsync();
foreach (var tenant in tenants)
foreach (var tenant in tenants.Where(t => t.ConnectionStrings.Any()))
{
i++;
using (_currentTenant.Change(tenant.Id))
{
Logger.LogInformation($"Migrating {tenant.Name} database schema... ({i} of {tenants.Count})");
Logger.LogInformation($"Migrating {tenant.Name} database schema... ({++i} of {tenants.Count})");
await MigrateTenantDatabasesAsync(tenant);
Logger.LogInformation($"Successfully completed {tenant.Name} database migrations.");
}

Loading…
Cancel
Save