-`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules.
+`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `createRoutes` methods of the packages.
```js
export function myPropEditContributor(
diff --git a/docs/en/framework/ui/angular/ellipsis-directive.md b/docs/en/framework/ui/angular/ellipsis-directive.md
index c462331574..df6db94ec5 100644
--- a/docs/en/framework/ui/angular/ellipsis-directive.md
+++ b/docs/en/framework/ui/angular/ellipsis-directive.md
@@ -5,47 +5,26 @@ Text inside an HTML element can be truncated easily with an ellipsis by using CS
## Getting Started
-In order to use the `EllipsisDirective` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this:
+In order to use the `EllipsisDirective` in an HTML template, it should be imported in your component. The selector of directive is **`abpEllipsis`**. By adding the `abpEllipsis` attribute to an HTML element, you can activate the `EllipsisDirective` for the HTML element.
```js
// ...
-import { ThemeSharedModule } from '@abp/ng.theme.shared';
+import { EllipsisDirective } from '@abp/ng.theme.shared';
-@NgModule({
+@Component({
//...
- imports: [..., ThemeSharedModule],
+ imports: [EllipsisDirective],
+ template: `
+
+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam commodi quae aspernatur,
+ corporis velit et suscipit id consequuntur amet minima expedita cum reiciendis dolorum
+ cupiditate? Voluptas eaque voluptatum odio deleniti quo vel illum nemo accusamus nulla ratione
+ impedit dolorum expedita necessitatibus fugiat ullam beatae, optio eum cupiditate ducimus
+ architecto.
+
+ `
})
-export class MyFeatureModule {}
-```
-
-or **if you would not like to import** the `ThemeSharedModule`, you can import the **`EllipsisModule`** as shown below:
-
-
-```js
-// ...
-import { EllipsisModule } from '@abp/ng.theme.shared';
-
-@NgModule({
- //...
- imports: [..., EllipsisModule],
-})
-export class MyFeatureModule {}
-```
-
-## Usage
-
-The `EllipsisDirective` is very easy to use. The directive's selector is **`abpEllipsis`**. By adding the `abpEllipsis` attribute to an HTML element, you can activate the `EllipsisDirective` for the HTML element.
-
-See an example usage:
-
-```html
-
- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam commodi quae aspernatur,
- corporis velit et suscipit id consequuntur amet minima expedita cum reiciendis dolorum
- cupiditate? Voluptas eaque voluptatum odio deleniti quo vel illum nemo accusamus nulla ratione
- impedit dolorum expedita necessitatibus fugiat ullam beatae, optio eum cupiditate ducimus
- architecto.
-
+export class SampleComponent {}
```
The `abpEllipsis` attribute has been added to the `` element that containing very long text inside to activate the `EllipsisDirective`.
diff --git a/docs/en/framework/ui/angular/entity-action-extensions.md b/docs/en/framework/ui/angular/entity-action-extensions.md
index b65e23d6b7..26913229bf 100644
--- a/docs/en/framework/ui/angular/entity-action-extensions.md
+++ b/docs/en/framework/ui/angular/entity-action-extensions.md
@@ -14,7 +14,7 @@ In this example, we will add a "Click Me!" action and alert the current row's `u
### Step 1. Create Entity Action Contributors
-The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module:
+The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root application configuration:
```ts
// src/app/entity-action-contributors.ts
@@ -49,22 +49,22 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li
### Step 2. Import and Use Entity Action Contributors
-Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
+Import `identityEntityActionContributors` in your routing configuration and pass it to the static `configureRoutes` method for `identity` routes as seen below:
```js
-// src/app/app-routing.module.ts
+// src/app/app.routes.ts
// other imports
import { identityEntityActionContributors } from './entity-action-contributors';
-const routes: Routes = [
+export const APP_ROUTES: Routes = [
// other routes
{
path: 'identity',
loadChildren: () =>
- import('@abp/ng.identity').then(m =>
- m.IdentityModule.forLazy({
+ import('@abp/ng.identity').then(c =>
+ c.createRoutes({
entityActionContributors: identityEntityActionContributors,
})
),
@@ -74,11 +74,11 @@ const routes: Routes = [
];
```
-That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `IdentityModule`.
+That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `identity` package.
## How to Place a Custom Modal and Trigger It by Entity Actions
-Let's employ dependency injection to extend the functionality of `IdentityModule` and add a quick view action for the User entity. We will take a lazy-loaded approach.
+Let's employ dependency injection to extend the functionality of `identity` package and add a quick view action for the User entity. We will take a lazy-loaded approach.
@@ -117,16 +117,27 @@ Let's employ dependency injection to extend the functionality of `IdentityModule
};
```
-3. Create a parent component to the identity module.
+3. Create a parent component to the identity package.
```js
// src/app/identity-extended/identity-extended.component.ts
- import { IdentityUserDto } from '@abp/ng.identity';
+ import { LocalizationPipe } from '@abp/ng.core';
+ import { IdentityUserDto } from '@abp/ng.identity/proxy';
+ import { ModalCloseDirective, ModalComponent } from '@abp/ng.theme.shared';
+ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
+ import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-identity-extended',
templateUrl: './identity-extended.component.html',
+ imports: [
+ CommonModule,
+ ModalComponent,
+ RouterOutlet,
+ LocalizationPipe,
+ ModalCloseDirective
+ ]
})
export class IdentityExtendedComponent {
isUserQuickViewVisible: boolean;
@@ -184,55 +195,47 @@ Let's employ dependency injection to extend the functionality of `IdentityModule
```
-5. Add a module for the component and load `IdentityModule` as seen below:
+5. Add a routing configuration for the component as seen below:
```js
- // src/app/identity-extended/identity-extended.module.ts
+ // src/app/identity-extended/identity-extended.routes.ts
- import { CoreModule } from '@abp/ng.core';
- import { IdentityModule } from '@abp/ng.identity';
- import { ThemeSharedModule } from '@abp/ng.theme.shared';
- import { NgModule } from '@angular/core';
- import { RouterModule } from '@angular/router';
- import { identityEntityActionContributors } from './entity-action-contributors';
+ import { Routes } from '@angular/router';
import { IdentityExtendedComponent } from './identity-extended.component';
+ import { identityEntityActionContributors } from './entity-action-contributors';
- @NgModule({
- imports: [
- CoreModule,
- ThemeSharedModule,
- RouterModule.forChild([
+ export const createExtendedIdentityRoutes = (): Routes => [
+ {
+ path: '',
+ component: IdentityExtendedComponent,
+ children: [
{
path: '',
- component: IdentityExtendedComponent,
- children: [
- {
- path: '',
- loadChildren: () =>
- IdentityModule.forLazy({
- entityActionContributors: identityEntityActionContributors,
- }),
- },
- ],
+ loadChildren: () =>
+ import('@abp/ng.identity').then(c =>
+ c.createRoutes({
+ entityActionContributors: identityEntityActionContributors,
+ }),
+ ),
},
- ]),
- ],
- declarations: [IdentityExtendedComponent],
- })
- export class IdentityExtendedModule {}
+ ],
+ },
+ ];
```
-6. Load `IdentityExtendedModule` instead of `IdentityModule` in your root routing module.
+6. Use `createExtendedIdentityRoutes` instead of the `createRoutes` function in your root routing configuration.
+Since the routes are already lazily loaded in the `createExtendedIdentityRoutes` function, you can directly use its children array to avoid an unnecessary additional lazy-loading call.
+
```js
- // src/app/app-routing.module.ts
+ // src/app/app.routes.ts
- const routes: Routes = [
+ export const APP_ROUTES: Routes = [
// other routes
{
path: 'identity',
- loadChildren: () =>
- import('./identity-extended/identity-extended.module')
- .then(m => m.IdentityExtendedModule),
+ children: [
+ ...createExtendedIdentityRoutes()
+ ],
},
// other routes
@@ -387,7 +390,7 @@ export function reorderUserContributors(
### EntityActionContributorCallback\
-`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules.
+`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `createRoutes` methods of the packages.
```js
// lockUserContributor should have EntityActionContributorCallback type
diff --git a/docs/en/framework/ui/angular/entity-filters.md b/docs/en/framework/ui/angular/entity-filters.md
index 75a197b81b..45929c1ef9 100644
--- a/docs/en/framework/ui/angular/entity-filters.md
+++ b/docs/en/framework/ui/angular/entity-filters.md
@@ -5,27 +5,9 @@ Every CRUD page includes some sort of inputs to filter the listed data. Some of
## Setup
The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the entity filter components.
-You can either import the `CommercialUiModule` which contains other components as well as `AdvancedEntityFilters` or you can directly import the `AdvancedEntityFiltersModule` if you do not need other components. Here is how you import them in your Angular module:
-
-```javascript
-import {
- CommercialUiModule,
- AdvancedEntityFiltersModule,
-} from "@volo/abp.commercial.ng.ui";
-
-@NgModule({
- imports: [
- // other imports
- CommercialUiModule,
-
- // OR
-
- AdvancedEntityFiltersModule,
- ],
- // rest of the module metadata
-})
-export class YourModule {}
-```
+
+Advanced entity filters are composed of several components: `AdvancedEntityFiltersComponent`, `AdvancedEntityFiltersToggleComponent`, `AdvancedEntityFiltersFormComponent`, and `AdvancedEntityFiltersAboveSearchComponent`. You can use these components directly by importing them into your standalone components.
+
## Usage
@@ -33,7 +15,7 @@ Let's take a look at the `Users` page from the `Identity` module.

-As shown in the screenshot, `abp-advanced-entity-filters` usually contain two parts, an entity filter (common among entities), i.e. `abp-entity-filter`, and entity-specific filters which are encapsulated within the `abp-advanced-entity-filters-form` component.
+As shown in the screenshot, `abp-advanced-entity-filters` usually contain two parts, an entity filter (common among entities), i.e. `abp-entity-filter`, and entity-specific filters which are encapsulated within the `abp-advanced-entity-filters-form` component. You will need to add `AdvancedEntityFiltersComponent` and `AdvancedEntityFiltersFormComponent` to your components' imports array to be able to use them.
`users.component.html`
@@ -72,7 +54,7 @@ As shown in the screenshot, `abp-advanced-entity-filters` usually contain two pa
```
-The `abp-advanced-entity-filters` already contains the `abp-entity-filter` component so you do not need to pass it. However, the `abp-entity-filter` component needs an instance of `ListService` which is usually stored in the `list` field of the page. You can also change the placeholder of the component via `entityFilterPlaceholder` input which is passed into the `abpLocalization` pipe so that it uses the translated text. Default is `'AbpUi::PagerSearch'`
+The `abp-advanced-entity-filters` already contains the `abp-entity-filter` component so you do not need to pass it. However, the `abp-entity-filter` component needs an instance of `ListService` which is usually stored in the `list` field of the page. You can also change the placeholder of the component via `entityFilterPlaceholder` input which is passed into the `abpLocalization` pipe so that it uses the translated text. The default is `'AbpUi::PagerSearch'`
E.g
@@ -100,8 +82,7 @@ E.g.
Let's remove `form` from the `Users` page
```html
-
-
+
```

@@ -122,7 +103,7 @@ E.g.

-Last but not least, if you need to render some content above the `abp-entity-filter` component, you can use the `abp-advanced-entity-filters-above-search`.
+Last but not least, if you need to render some content above the `abp-entity-filter` component, you can use the `abp-advanced-entity-filters-above-search`. This time, you will need to add `AdvancedEntityFiltersComponent`, `AdvancedEntityFiltersFormComponent`, and `AdvancedEntityFiltersAboveSearchComponent` to the imports' array of your component.
E.g.
diff --git a/docs/en/framework/ui/angular/environment.md b/docs/en/framework/ui/angular/environment.md
index 7fd1b1273a..8925b2b9d6 100644
--- a/docs/en/framework/ui/angular/environment.md
+++ b/docs/en/framework/ui/angular/environment.md
@@ -101,22 +101,28 @@ export interface RemoteEnv {
- `method`: HTTP method to be used when retrieving environment config. Default: `GET`
- `headers`: If extra headers are needed for the request, it can be set through this field.
-## Provide Environment Variable to Core Module
+## Configure Core Provider with Environment
`environment` variable comes from angular host application.
```js
import { environment } from '../environments/environment';
-@NgModule({
- imports: [
- //...other imports
- CoreModule.forRoot({
- environment
- }),
- ]
-})
+export const appConfig: ApplicationConfig = {
+ providers: [
+ ...
+ provideAbpCore(
+ withOptions({
+ environment,
+ ...
+ })
+ ),
+ ...
+ ],
+};
+
```
+
## EnvironmentService
` EnvironmentService` is a singleton service, i.e. provided in root level of your application, and keeps the environment in the internal store.
@@ -132,7 +138,7 @@ import {Â EnvironmentService } from '@abp/ng.core';
/* class metadata here */
})
class DemoComponent {
- constructor(private environment: EnvironmentService) {}
+ private environment = inject(EnvironmentService);
}
```
diff --git a/docs/en/framework/ui/angular/extensions-overall.md b/docs/en/framework/ui/angular/extensions-overall.md
index 4776198001..7fbf81126d 100644
--- a/docs/en/framework/ui/angular/extensions-overall.md
+++ b/docs/en/framework/ui/angular/extensions-overall.md
@@ -21,8 +21,8 @@ Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible t
[actionsColumnWidth]="38"
[actionsTemplate]="customAction"
[list]="list"
- (tableActivate)="onTableSelect($event)" >
-
+ (tableActivate)="onTableSelect($event)"
+ />
````
* ` actionsText : ` ** Column name of action column. **Type** : string
diff --git a/docs/en/framework/ui/angular/feature-libraries.md b/docs/en/framework/ui/angular/feature-libraries.md
index cda5f66551..07a72e0297 100644
--- a/docs/en/framework/ui/angular/feature-libraries.md
+++ b/docs/en/framework/ui/angular/feature-libraries.md
@@ -4,10 +4,10 @@ ABP has an ever-growing number of feature modules and [introducing a new one](..
## Feature Library Content
-Each library has at least two modules:
+Each library has at least two key elements:
-1. The main module contains all components, services, types, enums, etc. to deliver the required UI when the feature is loaded. From here on, we will refer to these modules as **"feature module"**.
-2. There is also a **"config module"** per library which helps us configure applications to run these modules or make them accessible.
+1. A **feature definition** that encapsulates all components, services, types, enums, and routing logic needed to deliver the UI for a given feature. With standalone structure, this is often expressed through a `routes.ts` file and associated components, and we will refer to this as the **"feature structure"**.
+2. A **configuration provider** that exposes setup logic, such as `provideMyProjectNameConfig()` functions or environment, specific tokens—allowing the feature to be initialized or integrated differently across applications. We will refer to this as the **configuration structure**.
## How to Add a Feature Library to Your Project
@@ -37,55 +37,46 @@ yarn add @abp/ng.identity
> Identity is used just as an example. If you have initiated your project with ABP CLI or ABP Suite, the identity library will already be installed and configured in your project.
-### 2. Import the Config Module
+### 2. Import the Configuration Provider
-As of ABP v3.0, every lazy-loaded module has a config module available via a secondary entry point on the same package. Importing them in your root module looks like this:
+As of ABP v9.3, every lazy-loaded route has a config provider available via a secondary entry point on the same package. Importing them in your root configuration looks like this:
```ts
import { provideIdentityConfig } from "@abp/ng.identity/config";
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
- // other imports
+ // other providers
provideIdentityConfig(),
],
- // providers, declarations, and bootstrap
-})
-export class AppModule {}
+};
```
-We need the config modules for actions required before feature modules are loaded (lazily). For example, the above import configures the menu to display links to identity pages.
+We need the config providers for actions required before feature structure is loaded (lazily). For example, the above import configures the menu to display links to identity pages.
-Furthermore, depending on the library, the `.forRoot` static method may receive some options that configure how the feature works.
+Furthermore, depending on the library, the `.createRoutes` static method may receive some options that configure how the feature works.
-### 3. Import the Feature Module
+### 3. Import the Feature Definition
-Finally, the feature module should be [loaded lazily via Angular router](https://angular.io/guide/lazy-loading-ngmodules). If you open the `/src/app/app-routing.module.ts` file, you should see `IdentityModule` is loaded exactly as follows:
+Finally, the feature structure should be [loaded lazily via Angular router](https://angular.dev/reference/migrations/route-lazy-loading). In a standalone setup, routing is typically defined in a `app.routes.ts` file, and feature modules are replaced with route-level feature definitions. You should see the identity routes configured like this:
```js
-import { NgModule } from "@angular/core";
-import { RouterModule, Routes } from "@angular/router";
+import { Routes } from "@angular/router";
-const routes: Routes = [
+const APP_ROUTES: Routes = [
// other routes
{
path: "identity",
loadChildren: () =>
- import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()),
+ import("@abp/ng.identity").then((m) => m.createRoutes()),
},
// other routes
];
-
-@NgModule({
- imports: [RouterModule.forRoot(routes)],
- exports: [RouterModule],
-})
-export class AppRoutingModule {}
```
When you load the identity feature like this, the "Users" page, for example, will have a route path of `/identity/users`. [1](#f-modify-route)
-Depending on the library, the `.forLazy` static method may also receive some options that configure how the feature works.
+Depending on the library, the `.createRoutes` static method may also receive some options that configure how the feature works.
---
diff --git a/docs/en/framework/ui/angular/form-validation.md b/docs/en/framework/ui/angular/form-validation.md
index 1c3c1a0d49..6a67018124 100644
--- a/docs/en/framework/ui/angular/form-validation.md
+++ b/docs/en/framework/ui/angular/form-validation.md
@@ -6,31 +6,31 @@ Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npm
## How to Add New Error Messages
-You can add a new error message by passing validation options to the `withValidationBluePrint` method of `provideAbpThemeShared` function in your root module.
+You can add a new error message by passing validation options to the `withValidationBluePrint` method inside `provideAbpThemeShared` function in your root application configuration.
```ts
import { provideAbpThemeShared, withValidationBluePrint } from '@abp/ng.theme.shared';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
+ // ...
provideAbpThemeShared(
withValidationBluePrint({
uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]"
})
),
- ...
+ // ...
],
-})
-export class AppModule {}
+};
```
-Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
+Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root configuration. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
{
provide: VALIDATION_BLUEPRINTS,
@@ -42,10 +42,7 @@ import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
// other providers
],
-
- // rest of the module metadata
-})
-export class AppModule {}
+};
```
When a [validator](https://angular.io/guide/form-validation#defining-custom-validators) or an [async validator](https://angular.io/guide/form-validation#creating-asynchronous-validators) returns an error with the key given to the error blueprints (`uniqueUsername` here), the validation library will be able to display an error message after localizing according to the given key and interpolation params. The result will look like this:
@@ -61,7 +58,7 @@ In this example;
## How to Change Existing Error Messages
-You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs.
+You can overwrite an existing error message by passing validation options to the `provideAbpThemeShared` in your root application configuration. Let's imagine you have a custom localization resource for required inputs.
```json
"RequiredInput": "Oops! We need this input."
@@ -72,24 +69,26 @@ To use this instead of the built-in required input message, all you need to do i
```ts
import { provideAbpThemeShared, withValidationBluePrint } from '@abp/ng.theme.shared';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
- provideAbpThemeShared(withValidationBluePrint({
- required: "::RequiredInput",
- })),
- ...
+ // ...
+ provideAbpThemeShared(
+ withValidationBluePrint({
+ required: "::RequiredInput",
+ })
+ ),
+ // ...
],
-})
-export class AppModule {}
+};
```
-Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
+Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root app configuration. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
{
provide: VALIDATION_BLUEPRINTS,
@@ -101,10 +100,7 @@ import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
// other providers
],
-
- // rest of the module metadata
-})
-export class AppModule {}
+};
```
The error message will look like this:
@@ -134,11 +130,14 @@ Validation works on any element or component with a `formControl` or `formContro
First, build a custom error component. Extending the existing `ValidationErrorComponent` would make it easier.
```js
+import { LocalizationPipe } from "@abp/ng.core";
import { ValidationErrorComponent } from "@abp/ng.theme.basic";
+import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component } from "@angular/core";
@Component({
selector: "app-validation-error",
+ imports:[CommonModule, LocalizationPipe],
template: `
+
```
diff --git a/docs/en/framework/ui/angular/localization.md b/docs/en/framework/ui/angular/localization.md
index 4ff0744212..6d4b3ab411 100644
--- a/docs/en/framework/ui/angular/localization.md
+++ b/docs/en/framework/ui/angular/localization.md
@@ -9,7 +9,7 @@ The Localization key format consists of 2 sections which are **Resource Name** a
```js
const environment = {
- //...
+ // ...
localization: {
defaultResourceName: "MyProjectName",
},
@@ -49,7 +49,7 @@ Localization data is stored in key-value pairs:
```js
{
- //...
+ // ...
AbpAccount: { // AbpAccount is the resource name
Key: "Value",
PagerInfo: "Showing {0} to {1} of {2} entries"
@@ -121,12 +121,12 @@ See an example:
```ts
import { provideAbpCore, withOptions } from '@abp/ng.core';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
// ...
provideAbpCore(
withOptions({
- ...,
+ // ...,
localizations: [
{
culture: 'en',
@@ -155,22 +155,18 @@ import { provideAbpCore, withOptions } from '@abp/ng.core';
],
}),
),
- ...
],
-})
-export class AppModule {}
+};
```
-...or, you can determine the localizations in a feature module:
+...or, you can determine the localizations in a feature provider configuration:
```ts
-// your feature module
+// your feature configuration
-@NgModule({
- imports: [
- //...other imports
- CoreModule.forChild({
- localizations: [
+export function provideFeatureConfiguration(): EnvironmentProviders{
+ return provideAbpCoreChild({
+ localizations: [
{
culture: 'en',
resources: [
@@ -196,9 +192,8 @@ export class AppModule {}
],
},
],
- }),
- ]
-})
+ })
+}
```
The localizations above can be used like this:
@@ -267,8 +262,8 @@ import { Component } from "@angular/core";
@Component({
selector: "app-root",
template: `
-
-
+
+
`,
})
export class AppComponent {}
@@ -276,7 +271,7 @@ export class AppComponent {}
## Registering a New Locale
-Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `CoreModule`.
+Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `provideAbpCore(withOptions({...}))`.
### registerLocaleFn
@@ -286,11 +281,12 @@ Since ABP has more than one language, Angular locale files loads lazily using [W
import { provideAbpCore, withOptions } from '@abp/ng.core';
import { registerLocale } from '@abp/ng.core/locale';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
+ // ...
provideAbpCore(
withOptions({
- ...,
+ // ...,
registerLocaleFn: registerLocale(
// you can pass the cultureNameLocaleFileMap and errorHandlerFn as optionally
{
@@ -302,10 +298,9 @@ import { registerLocale } from '@abp/ng.core/locale';
),
}),
),
- ...
+ // ...
],
-})
-export class AppModule {}
+};
```
### Mapping of Culture Name to Angular Locale File Name
@@ -317,19 +312,18 @@ Some of the culture names defined in .NET do not match Angular locales. In such
If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to the `registerLocale` function.
```js
-// app.module.ts
+// app.config.ts
import { registerLocale } from '@abp/ng.core/locale';
// if you have commercial license and the language management module, add the below import
// import { registerLocale } from '@volo/abp.ng.language-management/locale';
-
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
// ...
provideAbpCore(
withOptions({
- ...,
+ // ...,
registerLocaleFn: registerLocale(
{
cultureNameLocaleFileMap: {
@@ -340,18 +334,18 @@ import { registerLocale } from '@abp/ng.core/locale';
)
}),
),
- ]
-})
+ ],
+};
```
See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales).
### Adding a New Culture
-Add the below code to the `app.module.ts` by replacing `your-locale` placeholder with a correct locale name.
+Add the below code to the `app.config.ts` by replacing `your-locale` placeholder with a correct locale name.
```js
-//app.module.ts
+//app.config.ts
import { storeLocaleData } from "@abp/ng.core/locale";
import(
@@ -361,7 +355,7 @@ import(
).then((m) => storeLocaleData(m.default, "your-locale"));
```
-...or a custom `registerLocale`Â function can be passed to the `CoreModule`:
+...or a custom `registerLocale`Â function can be passed to the abp core provider configuration options:
```js
// register-locale.ts
@@ -376,22 +370,22 @@ export function registerLocale(locale: string) {
)
}
-// app.module.ts
+// app.config.ts
import { registerLocale } from './register-locale';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
- // ...
+ // ...
provideAbpCore(
withOptions({
- ...,
+ // ...,
registerLocaleFn: registerLocale,
}),
),
//...
- ]
-})
+ ],
+};
```
After this custom `registerLocale` function, since the en and fr added to the `webpackInclude`, only en and fr locale files will be created as chunks:
diff --git a/docs/en/framework/ui/angular/lookup-components.md b/docs/en/framework/ui/angular/lookup-components.md
index ce1e32eefd..e63d2b3700 100644
--- a/docs/en/framework/ui/angular/lookup-components.md
+++ b/docs/en/framework/ui/angular/lookup-components.md
@@ -4,22 +4,7 @@ The Angular UI of ABP Commercial introduces some components with `abp-lookup-...
## Setup
-The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. Here is how you import them in your Angular module:
-
-```javascript
-import { CommercialUiModule } from '@volo/abp.commercial.ng.ui';
-
-@NgModule({
- imports: [
- // other imports
- CommercialUiModule,
- ],
- // rest of the module metadata
-})
-export class YourModule {}
-```
-
-Now you can use the lookup components in your components declared by this module.
+The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. You can import these in your standalone components in order to be able to use them.
## Lookup HTTP Requests
@@ -48,7 +33,7 @@ Typeahead is a good choice when you have an unknown number of records for the re

-Here is how it is used in the template.
+Do not forget to import `LookupTypeaheadComponent` in your component, and here is how it is used in the template.
```html
+/>
```
The available properties are as follows:
@@ -77,7 +62,7 @@ Select is a good choice when you have a low (and usually fixed) number of record

-Here is how it is used in the template.
+Do not forget to import `LookupSelectComponent` in your component, and here is how it is used in the template.
```html
+/>
```
The available properties are as follows:
diff --git a/docs/en/framework/ui/angular/manage-profile-page-tabs.md b/docs/en/framework/ui/angular/manage-profile-page-tabs.md
index 232575a419..fbb12c852e 100644
--- a/docs/en/framework/ui/angular/manage-profile-page-tabs.md
+++ b/docs/en/framework/ui/angular/manage-profile-page-tabs.md
@@ -9,7 +9,7 @@ See the example below, covers all features:
```ts
// manage-profile-tabs.provider.ts
-import { APP_INITIALIZER, Component } from "@angular/core";
+import { provideAppInitializer, Component } from "@angular/core";
import { TwoFactorTabComponent } from "@volo/abp.ng.account/public";
import {
eAccountManageProfileTabNames,
@@ -18,48 +18,47 @@ import {
import { MyAwesomeTabComponent } from "./my-awesome-tab/my-awesome-tab.component";
@Component({
- standalone: true,
selector: "abp-my-awesome-tab",
template: `My Awesome Tab`,
})
class MyAwesomeTabComponent {}
export const MANAGE_PROFILE_TAB_PROVIDER = {
- provide: APP_INITIALIZER,
- useFactory: configureManageProfileTabs,
- deps: [ManageProfileTabsService],
- multi: true,
+ provideAppInitializer(()=>{
+ configureManageProfileTabs();
+ }),
};
-export function configureManageProfileTabs(tabs: ManageProfileTabsService) {
- return () => {
- tabs.add([
- {
- name: "::MyAwesomeTab", // supports localization keys
- order: 5,
- component: MyAwesomeTabComponent,
- },
- ]);
-
- tabs.patch(eAccountManageProfileTabNames.TwoFactor, {
- name: "Two factor authentication",
- component: TwoFactorTabComponent,
- });
-
- tabs.remove([eAccountManageProfileTabNames.ProfilePicture]);
- };
+export function configureManageProfileTabs() {
+ tabs = inject(ManageProfileTabsService);
+ tabs.add([
+ {
+ name: "::MyAwesomeTab", // supports localization keys
+ order: 5,
+ component: MyAwesomeTabComponent,
+ },
+ ]);
+
+ tabs.patch(eAccountManageProfileTabNames.TwoFactor, {
+ name: "Two factor authentication",
+ component: TwoFactorTabComponent,
+ });
+
+ tabs.remove([eAccountManageProfileTabNames.ProfilePicture]);
}
```
```ts
-//app.module.ts
+//app.config.ts
import { MANAGE_PROFILE_TAB_PROVIDER } from "./manage-profile-tabs.provider";
-@NgModule({
- providers: [MANAGE_PROFILE_TAB_PROVIDER],
-})
-export class AppModule {}
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ MANAGE_PROFILE_TAB_PROVIDER
+ ],
+};
```
What we have done above;
@@ -70,7 +69,7 @@ What we have done above;
- Renamed the "Two factor" tab label.
- Removed the "Profile picture" tab.
- Determined the `MANAGE_PROFILE_TAB_PROVIDER` to be able to run the `configureManageProfileTabs` function on initialization.
-- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `AppModule` providers.
+- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `appConfig` providers.
See the result:
diff --git a/docs/en/framework/ui/angular/modal.md b/docs/en/framework/ui/angular/modal.md
index 2b8412202c..3691001c79 100644
--- a/docs/en/framework/ui/angular/modal.md
+++ b/docs/en/framework/ui/angular/modal.md
@@ -15,27 +15,33 @@ The `abp-modal` provides some additional benefits:
## Getting Started
-In order to use the `abp-modal` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this:
+In order to use the `abp-modal` in an HTML template, the **`ModalComponent`** should be imported into your component like this:
```js
+// sample.component.ts
// ...
-import { ThemeSharedModule } from '@abp/ng.theme.shared';
+import { ModalComponent, ModalCloseDirective } from '@abp/ng.theme.shared';
-@NgModule({
+@Component({
//...
- imports: [..., ThemeSharedModule],
+ ,
+ imports: [
+ // ...,
+ ModalComponent,
+ ModalCloseDirective // if you use `abpClose` directive in the html template
+ ],
})
-export class MyFeatureModule {}
+export class SampleComponent {
+ isModalOpen = false;
+}
```
-## Usage
-
-You can add the `abp-modal`Â to your component very quickly. See an example:
-
```html
-Open modal
+
+ Open modal
+
@@ -43,24 +49,17 @@ You can add the `abp-modal`Â to your component very quickly. See an example:
- Modal content
+ Modal content
- Close
+
+ Close
+
```
-```js
-// sample.component.ts
-
-@Component(/* component metadata */)
-export class SampleComponent {
- isModalOpen = false
-}
-```
-

@@ -136,7 +135,10 @@ import { FormBuilder, Validators } from '@angular/forms';
@Component(/* component metadata */)
export class BookComponent {
- form = this.fb.group({
+ private fb = inject(FormBuilder);
+ private service = inject(BookService);
+
+ form = this.fb.group({
author: [null, [Validators.required]],
name: [null, [Validators.required]],
price: [null, [Validators.required, Validators.min(0)]],
@@ -148,10 +150,10 @@ export class BookComponent {
isModalOpen: boolean;
- constructor(private fb: FormBuilder, private service: BookService) {}
-
save() {
- if (this.form.invalid) return;
+ if (this.form.invalid) {
+ return;
+ }
this.inProgress = true;
@@ -257,19 +259,16 @@ export class NgbdModalOptions {
**`suppressUnsavedChangesWarning`** is a boolean input that determines whether the confirmation popup triggering active or not. It can also be set globally as shown below:
```ts
-//app.module.ts
-
-// app.module.ts
+// app.config.ts
import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '@abp/ng.theme.shared';
-// ...
-
-@NgModule({
- // ...
- providers: [{provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true}]
-})
-export class AppModule {}
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ { provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true }
+ ],
+};
```
Note: The `suppressUnsavedChangesWarning` input of `abp-modal` value overrides the `SUPPRESS_UNSAVED_CHANGES_WARNING` injection token value.
diff --git a/docs/en/framework/ui/angular/modifying-the-menu.md b/docs/en/framework/ui/angular/modifying-the-menu.md
index ef5e12782f..ee3997b9d7 100644
--- a/docs/en/framework/ui/angular/modifying-the-menu.md
+++ b/docs/en/framework/ui/angular/modifying-the-menu.md
@@ -59,32 +59,33 @@ An alternative and probably cleaner way is to use a route provider. First create
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
-import { APP_INITIALIZER } from '@angular/core';
+import { provideAppInitializer } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
- { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
+ provideAppInitializer(() => {
+ configureRoutes();
+ }),
];
-function configureRoutes(routes: RoutesService) {
- return () => {
- routes.add([
- {
- path: '/your-path',
- name: 'Your navigation',
- requiredPolicy: 'permission key here',
- order: 101,
- iconClass: 'fas fa-question-circle',
- layout: eLayoutType.application,
- },
- {
- path: '/your-path/child',
- name: 'Your child navigation',
- parentName: 'Your navigation',
- requiredPolicy: 'permission key here',
- order: 1,
- },
- ]);
- };
+function configureRoutes() {
+ const routesService = inject(RoutesService);
+ routes.add([
+ {
+ path: '/your-path',
+ name: 'Your navigation',
+ requiredPolicy: 'permission key here',
+ order: 101,
+ iconClass: 'fas fa-question-circle',
+ layout: eLayoutType.application,
+ },
+ {
+ path: '/your-path/child',
+ name: 'Your child navigation',
+ parentName: 'Your navigation',
+ requiredPolicy: 'permission key here',
+ order: 1,
+ },
+ ]);
}
```
@@ -95,22 +96,21 @@ We can also define a group for navigation elements. It's an optional property
// route.provider.ts
import { RoutesService } from '@abp/ng.core';
-function configureRoutes(routes: RoutesService) {
- return () => {
- routes.add([
- {
- //etc..
- group: 'ModuleName::GroupName'
- },
- {
- path: '/your-path/child',
- name: 'Your child navigation',
- parentName: 'Your navigation',
- requiredPolicy: 'permission key here',
- order: 1,
- },
- ]);
- };
+function configureRoutes() {
+ const routesService = inject(RoutesService);
+ routes.add([
+ {
+ //etc..
+ group: 'ModuleName::GroupName'
+ },
+ {
+ path: '/your-path/child',
+ name: 'Your child navigation',
+ parentName: 'Your navigation',
+ requiredPolicy: 'permission key here',
+ order: 1,
+ },
+ ]);
}
```
@@ -131,25 +131,23 @@ export class AppComponent {
}
```
-...and then in app.module.ts...
+...and then in app.config.ts...
- The `groupedVisible` method will return the `Others` group for ungrouped items, the default key is `AbpUi::OthersGroup`, we can change this `key` via the `OTHERS_GROUP` injection token
```js
-import { NgModule } from '@angular/core';
import { OTHERS_GROUP } from '@abp/ng.core';
import { APP_ROUTE_PROVIDER } from './route.provider';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
+ // ...
APP_ROUTE_PROVIDER,
{
provide: OTHERS_GROUP,
useValue: 'ModuleName::MyOthersGroupKey',
},
],
- // imports, declarations, and bootstrap
-})
-export class AppModule {}
+};
```
### Singularize Route Item
@@ -182,9 +180,9 @@ Here is what every property works as:
- `invisible` makes the item invisible in the menu. (default: `false`)
- `group` is an optional property that is used to group together related routes in an application. (type: `string`, default: `AbpUi::OthersGroup`)
-### Via `routes` Property in `AppRoutingModule`
+### Via `routes` Property in `APP_ROUTES`
-You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `RoutesService`.
+You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app.routes.ts`. The `@abp/ng.core` package organizes your routes and stores them in the `RoutesService`.
You can add the `routes` property like below:
diff --git a/docs/en/framework/ui/angular/multi-tenancy.md b/docs/en/framework/ui/angular/multi-tenancy.md
index bfb929cc68..c85ac976a3 100644
--- a/docs/en/framework/ui/angular/multi-tenancy.md
+++ b/docs/en/framework/ui/angular/multi-tenancy.md
@@ -133,12 +133,9 @@ Example:
```ts
import { TENANT_NOT_FOUND_BY_NAME } from '@abp/ng.core';
-@NgModule({
- imports: [
- // removed for clarity
- ],
+export const appConfig: ApplicationConfig = {
providers: [
- // removed for clarity
+ // removed for clarity
{
provide: TENANT_NOT_FOUND_BY_NAME,
useFactory: function () {
@@ -148,11 +145,7 @@ import { TENANT_NOT_FOUND_BY_NAME } from '@abp/ng.core';
},
},
],
- declarations: [AppComponent],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
-
+};
```
## See Also
diff --git a/docs/en/framework/ui/angular/oauth-module.md b/docs/en/framework/ui/angular/oauth-module.md
index 0e67d44d07..8ec30ae4b4 100644
--- a/docs/en/framework/ui/angular/oauth-module.md
+++ b/docs/en/framework/ui/angular/oauth-module.md
@@ -2,8 +2,7 @@
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0.
-If your app is version 8.3 or higher, you should include "provideAbpOAuth()" in your app.module.ts as an providers after "provideAbpCore()
-".
+If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`.
Those abstractions can be found in the @abp/ng-core packages.
diff --git a/docs/en/framework/ui/angular/page-component.md b/docs/en/framework/ui/angular/page-component.md
index b9fbc0b0bc..f190597fcf 100644
--- a/docs/en/framework/ui/angular/page-component.md
+++ b/docs/en/framework/ui/angular/page-component.md
@@ -14,10 +14,10 @@ Let's look at the following example without `abp-page` component.
@@ -42,22 +42,19 @@ export enum PageParts {
## Usage
-Firstly, you need to import `PageModule` from `@abp/ng.components/page` as follows:
+Firstly, you need to import Page components from `@abp/ng.components/page` based on your usage. Here is an example:
-`dashboard.module.ts`
+`dashboard.component.ts`
```javascript
-import {Â PageModule } from '@abp/ng.components/page';
-import {Â DashboardComponent } from './dashboard.component';
-@NgModule({
- declarations: [DashboardComponent],
- imports: [PageModule]
+@Component({
+ imports: [ PageComponent, ... ]
})
-export class DashboardModule {}
+export class  DashboardComponent {}
```
-And change the template of `dashboard.component.ts` to the following:
+And change the template of `dashboard.component.html` to the following:
```html
@@ -75,35 +72,55 @@ And change the template of `dashboard.component.ts` to the following:
## Overriding template
-If you need to replace the template of any part, you can use the following sub-components.
+If you need to replace the template of any part, you can use the following sub-components. You will need to import these components and modify the html template accordingly.
-```html
-
-
- Custom Title
-
-
-
-
-
-
-
- Some Action
-
-
+```javascript
+import {
+ PageComponent,
+ PageTitleContainerComponent,
+ PageBreadcrumbContainerComponent,
+ PageToolbarContainerComponent
+} from '@abp/ng.components/page';
+
+@Component({
+ selector: 'app-sample-component',
+ template: `
+
+
+ Custom Title
+
+
+
+
+
+
+
+ Some Action
+
+
+ `
+ imports: [
+ PageComponent,
+ PageTitleContainerComponent,
+ PageBreadcrumbContainerComponent,
+ MyBreadcrumbComponent,
+ PageToolbarContainerComponent
+ ]
+})
+export class SampleCompnent {}
```
You do not have to provide them all. You can just use which one you need to replace. These components have priority over the inputs declared above. If you use these components, you can omit the inputs.
## PagePartDirective
-`PageModule` provides a structural directive that is used internally within `PageComponent` and can also be used externally.
+`Components` package provides a structural directive that is used internally within `PageComponent` and can also be used externally.
`PageComponent` employs this directive internally as follows:
```html
```
@@ -111,7 +128,7 @@ It also can take a context input as follows:
```html
```
@@ -194,17 +211,14 @@ export class MyPageRenderStrategy implements PageRenderStrategy {
})
export class DashboardComponent {}
-@NgModule({
- imports: [PageModule],
- declarations: [DashboardComponent],
+export const appConfig: ApplicationConfig = {
providers: [
{
provide: PAGE_RENDER_STRATEGY,
useClass: MyPageRenderStrategy,
}
- ]
-})
-export class DashboardModule {}
+ ],
+};
```
## See Also
diff --git a/docs/en/framework/ui/angular/page-toolbar-extensions.md b/docs/en/framework/ui/angular/page-toolbar-extensions.md
index 74f79178dd..5a741b3d2f 100644
--- a/docs/en/framework/ui/angular/page-toolbar-extensions.md
+++ b/docs/en/framework/ui/angular/page-toolbar-extensions.md
@@ -14,7 +14,7 @@ In this example, we will add a "Click Me!" action and log `userName` of all user
### Step 1. Create Toolbar Action Contributors
-The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module:
+The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root application configuration:
```js
// src/app/toolbar-action-contributors.ts
@@ -53,22 +53,22 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li
### Step 2. Import and Use Toolbar Action Contributors
-Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
+Import `identityToolbarActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` route as seen below:
```js
-// src/app/app-routing.module.ts
+// src/app/app.routes.ts
// other imports
import { identityToolbarActionContributors } from './toolbar-action-contributors';
-const routes: Routes = [
+export const APP_ROUTES: Routes = [
// other routes
{
path: 'identity',
loadChildren: () =>
- import('@abp/ng.identity').then(m =>
- m.IdentityModule.forLazy({
+ import('@abp/ng.identity').then(c =>
+ c.createRoutes({
toolbarActionContributors: identityToolbarActionContributors,
})
),
@@ -78,7 +78,7 @@ const routes: Routes = [
];
```
-That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`.
+That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package.
## How to Add a Custom Component to Page Toolbar
@@ -93,9 +93,9 @@ We need to have a component before we can pass it to the toolbar action contribu
```js
// src/app/click-me-button.component.ts
+import { Component, Inject } from '@angular/core';
import { IdentityUserDto } from '@abp/ng.identity/proxy';
import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensible';
-import { Component, Inject } from '@angular/core';
@Component({
selector: 'app-click-me-button',
@@ -120,7 +120,7 @@ Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolb
### Step 2. Create Toolbar Action Contributors
-The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in:
+The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root application configuration. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in:
```js
// src/app/toolbar-action-contributors.ts
@@ -156,22 +156,22 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li
### Step 3. Import and Use Toolbar Action Contributors
-Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below.
+Import `identityToolbarActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` route as seen below.
```js
-// src/app/app-routing.module.ts
+// src/app/app.routes.ts
// other imports
import { identityToolbarActionContributors } from './toolbar-action-contributors';
-const routes: Routes = [
+export const APP_ROUTES: Routes = [
// other routes
{
path: 'identity',
loadChildren: () =>
- import('@abp/ng.identity').then(m =>
- m.IdentityModule.forLazy({
+ import('@abp/ng.identity').then(c =>
+ c.createRoutes({
toolbarActionContributors: identityToolbarActionContributors,
})
),
@@ -181,7 +181,7 @@ const routes: Routes = [
];
```
-That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components.
+That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components.
## How to Place a Custom Modal and Trigger It by Toolbar Actions
@@ -380,7 +380,7 @@ export const identityEntityActionContributors = {
### ToolbarActionContributorCallback\
-`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules.
+`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `createRoutes` methods of the packages.
```js
// exportUsersContributor should have ToolbarActionContributorCallback type
diff --git a/docs/en/framework/ui/angular/password-complexity-indicator-component.md b/docs/en/framework/ui/angular/password-complexity-indicator-component.md
index 806b686ece..1733f9543e 100644
--- a/docs/en/framework/ui/angular/password-complexity-indicator-component.md
+++ b/docs/en/framework/ui/angular/password-complexity-indicator-component.md
@@ -42,7 +42,7 @@ The `PasswordComplexityIndicatorService` is for calculating the password complex
It's easy, imagine you have a password input that you want to add the complexity indicator under. Put this component under the input
```ts
-
+
```
- Pass the password to the `validatePassword` method of the `PasswordComplexityIndicatorService`, and bind return the value to the `progressBar` property of the `abp-password-complexity-indicator`
diff --git a/docs/en/framework/ui/angular/permission-management.md b/docs/en/framework/ui/angular/permission-management.md
index c797402003..5d3ed64351 100644
--- a/docs/en/framework/ui/angular/permission-management.md
+++ b/docs/en/framework/ui/angular/permission-management.md
@@ -100,11 +100,10 @@ export class CustomPermissionService extends PermissionService {
}
```
-- Then, in `app.module.ts`, provide this service as follows:
+- Then, in `app.config.ts`, provide this service as follows:
```js
-@NgModule({
- // ...
+export const appConfig: ApplicationConfig = {
providers: [
// ...
{
@@ -112,9 +111,7 @@ export class CustomPermissionService extends PermissionService {
useExisting: CustomPermissionService,
},
],
- // ...
-})
-export class AppModule {}
+};
```
That's it. Now, when a directive/guard asks for `PermissionService` from angular, it will inject your service.
diff --git a/docs/en/framework/ui/angular/pwa-configuration.md b/docs/en/framework/ui/angular/pwa-configuration.md
index 918aa4970d..9825eb0a52 100644
--- a/docs/en/framework/ui/angular/pwa-configuration.md
+++ b/docs/en/framework/ui/angular/pwa-configuration.md
@@ -38,7 +38,7 @@ So, Angular CLI updates some files and add a few others:
- `serviceWorker` is `true` in production build.
- `ngswConfigPath` refers to _ngsw-config.json_.
- **package.json** has _@angular/service-worker_ as a new dependency.
-- **app.module.ts** imports `ServiceWorkerModule` and registers a service worker filename.
+- **app.config.ts** imports `ServiceWorkerModule` and registers a service worker filename.
- **index.html** has following modifications:
- A ` ` element that refers to _manifest.webmanifest_.
- A ` ` tag that sets a theme color.
diff --git a/docs/en/framework/ui/angular/quick-start.md b/docs/en/framework/ui/angular/quick-start.md
index 3dd6e2f32d..77080141ed 100644
--- a/docs/en/framework/ui/angular/quick-start.md
+++ b/docs/en/framework/ui/angular/quick-start.md
@@ -54,28 +54,27 @@ Here is what these folders and files are for:
- **.vscode** has extension recommendations in it.
- **e2e** is a separate app for possible end-to-end tests.
- **src** is where the source files for your application are placed. We will have a closer look in a minute.
-- **.browserlistrc** helps [configuring browser compatibility of your Angular app](https://angular.io/guide/build#configuring-browser-compatibility).
+- **.browserlistrc** helps [configuring browser compatibility of your Angular app](https://angular.dev/tools/cli/build#configuring-browser-compatibility).
- **.editorconfig** helps you have a shared coding style for separate editors and IDEs. Check [EditorConfig.org](https://editorconfig.org/) for details.
- **.gitignore** defined which files and folders should not be tracked by git. Check [git documentation](https://git-scm.com/docs/gitignore) for details.
- **.prettierrc** includes simple coding style choices for [Prettier](https://prettier.io/), an auto-formatter for TypeScript, HTML, CSS, and more. If you install recommended extensions to VS Code, you will never have to format your code anymore.
-- **angular.json** is where Angular workspace is defined. It holds project configurations and workspace preferences. Please refer to [Angular workspace configuration](https://angular.io/guide/workspace-config) for details.
+- **angular.json** is where Angular workspace is defined. It holds project configurations and workspace preferences. Please refer to [Angular workspace configuration](https://angular.dev/reference/configs/workspace-config) for details.
- **karma.conf.js** holds [Karma test runner](https://karma-runner.github.io/) configurations.
-- **package.json** is where your [package dependencies](https://angular.io/guide/npm-packages) are listed. It also includes some useful scripts for developing, testing, and building your application.
+- **package.json** is where your [package dependencies](https://angular.dev/reference/configs/npm-packages) are listed. It also includes some useful scripts for developing, testing, and building your application.
- **README.md** includes some of Angular CLI command examples. You either have to install Angular CLI globally or run these commands starting with `yarn` or `npx` to make them work.
-- **start.ps1** is a simple PowerShell script to install dependencies and start a [development server via Angular CLI](https://angular.io/cli/serve), but you probably will not need that after reading this document.
-- **tsconfig.json** and all other [tsconfig files](https://angular.io/guide/typescript-configuration) in general, include some TypeScript and Angular compile options.
+- **start.ps1** is a simple PowerShell script to install dependencies and start a [development server via Angular CLI](https://angular.dev/cli/serve), but you probably will not need that after reading this document.
+- **tsconfig.json** and all other [tsconfig files](https://angular.dev/reference/configs/angular-compiler-options) in general, include some TypeScript and Angular compile options.
- **yarn.lock** enables installing consistent package versions across different devices so that working application build will not break because of a package update. Please read [Yarn documentation](https://classic.yarnpkg.com/en/docs/yarn-lock/) if you are interested in more information on the topic. If you have decided to use npm, please remove this file and keep the [package-lock.json](https://docs.npmjs.com/files/package-lock.json) instead.
Now let us take a look at the contents of the source folder.
-- **app** is the main directory you put your application files in. Any module, component, directive, service, pipe, guard, interceptor, etc. should be placed here. You are free to choose any folder structure, but [organizing Angular applications based on modules](https://angular.io/guide/module-types) is generally a fine practice.
-- **home** is a predefined module and acts as a welcome page. It also demonstrates how a feature-based folder structure may look like. More complex features will probably have sub-features, thus inner folders. You may change the home folder however you like.
-- **shared** is spared for reusable code that works for several modules. Some, including yours truly, may disagree with using a single module for all shared code, so consider adding standalone sub-modules inside this folder instead of adding everything into **shared.module.ts**.
-- **app-routing.module.ts** is where your top-level routes are defined. Angular is capable of [lazy loading feature modules](https://angular.io/guide/lazy-loading-ngmodules), so not all routes will be here. You may think of Angular routing as a tree and this file is the top of the tree.
+- **app** is the main directory you put your application files in. Any component, directive, service, pipe, guard, interceptor, etc. should be placed here. You are free to choose any folder structure, but [organizing Angular applications using configuration-based structure](https://angular.dev/reference/configs/file-structure) is generally a fine practice, especially when using standalone APIs. This replaces the older convention of organizing strictly by NgModules.
+- **home** is a predefined component and acts as a welcome page. It also demonstrates how a feature-based folder structure may look like. More complex features will probably have sub-features, thus inner folders. You may change the home folder however you like.
+- **app.routes.ts** is where your top-level routes are defined. Angular is capable of [lazy loading routes now](https://angular.dev/reference/migrations/route-lazy-loading), so not all routes will be here. You may think of Angular routing as a tree and this file is the top of the tree.
- **app.component.ts** is essentially the top component that holds the dynamic application layout.
-- **app.module.ts** is the [root module](https://angular.io/guide/bootstrapping) that includes information about how parts of your application are related and what to run at the initiation of your application.
+- **app.config.ts** is the [root configuration](https://angular.dev/api/platform-browser/bootstrapApplication) that includes information about how parts of your application are related and what to run at the initiation of your application.
- **route.provider.ts** is used for [modifying the menu](../angular/modifying-the-menu.md).
- **assets** is for static files. A file (e.g. an image) placed in this folder will be available as is when the application is served.
- **environments** includes one file per environment configuration. There are two configurations by default, but you may always introduce another one. These files are directly referred to in _angular.json_ and help you have different builds and application variables. Please refer to [configuring Angular application environments](https://angular.io/guide/build#configuring-application-environments) for details.
@@ -155,7 +154,7 @@ export const environment = {
} as Config.Environment;
```
-When you run the development server, variables defined in _environment.ts_ take effect. Similarly, in production mode, the default environment is replaced by _environment.prod.ts_ and completely different variables become effective. You may even [create a new build configuration](https://angular.io/guide/workspace-config#build-configs) and set [file replacements](https://angular.io/guide/build#configure-target-specific-file-replacements) to use a completely new environment. For now, we will start a production build:
+When you run the development server, variables defined in _environment.ts_ take effect. Similarly, in production mode, the default environment is replaced by _environment.prod.ts_ and completely different variables become effective. You may even [create a new build configuration](https://angular.dev/reference/configs/workspace-config#alternate-build-configurations) and set [file replacements](https://angular.io/guide/build#configure-target-specific-file-replacements) to use a completely new environment. For now, we will start a production build:
1. Open your terminal and navigate to the root Angular folder.
2. Run `yarn` or `npm install` if you have not installed dependencies already.
diff --git a/docs/en/framework/ui/angular/show-password-directive.md b/docs/en/framework/ui/angular/show-password-directive.md
index cacb2550c5..588aa3a60b 100644
--- a/docs/en/framework/ui/angular/show-password-directive.md
+++ b/docs/en/framework/ui/angular/show-password-directive.md
@@ -4,24 +4,20 @@ In password input, text can be shown easily via changing input type attribute to
## Getting Started
-`ShowPasswordDirective` is standalone. In order to use the `ShowPasswordDirective` in an HTML template, import it to related module or your standalone component:
+`ShowPasswordDirective` is standalone. In order to use the it in an HTML template, import it to your component:
-**Importing to NgModule**
+**Importing to Component**
```ts
import { ShowPasswordDirective } from '@abp/ng.core';
-@NgModule({
+@Component({
//...
- declarations: [
- ...,
- TestComponent
- ],
imports: [
- ...,
+ // ...,
ShowPasswordDirective
],
})
-export class MyFeatureModule {}
+export class TestComponent {}
```
## Usage
@@ -30,28 +26,10 @@ The `ShowPasswordDirective` is very easy to use. The directive's selector is **`
See an example usage:
-**NgModule Component usage**
-```ts
-@Component({
- selector: 'test-component',
- template: `
-
- Password
-
- icon
-
- `
-})
-export class TestComponent{
- showPassword = false;
-}
-```
-**Standalone Component usage**
```ts
import { ShowPasswordDirective } from '@abp/ng.core';
@Component({
- selector: 'standalone-component',
- standalone: true,
+ selector: 'sample-component',
template: `
Password
@@ -61,7 +39,7 @@ import { ShowPasswordDirective } from '@abp/ng.core';
`,
imports: [ShowPasswordDirective]
})
-export class StandaloneComponent{
+export class SampleComponent{
showPassword = false;
}
```
diff --git a/docs/en/framework/ui/angular/sorting-navigation-elements.md b/docs/en/framework/ui/angular/sorting-navigation-elements.md
index 7385a1681c..a5a9b3db89 100644
--- a/docs/en/framework/ui/angular/sorting-navigation-elements.md
+++ b/docs/en/framework/ui/angular/sorting-navigation-elements.md
@@ -19,19 +19,18 @@ This documentation describes how the navigation elements are sorted and how to c
# How to Customize
-**`in app.module.ts`**
+**`in app.config.ts`**
```ts
import { SORT_COMPARE_FUNC } from "@abp/ng.core";
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
- ...{
+ // ...
+ {
provide: SORT_COMPARE_FUNC,
useFactory: yourCompareFuncFactory,
},
],
- // imports, declarations, and bootstrap
-})
-export class AppModule {}
+};
```
diff --git a/docs/en/framework/ui/angular/toaster-service.md b/docs/en/framework/ui/angular/toaster-service.md
index ad08697eeb..a6c65ccb72 100644
--- a/docs/en/framework/ui/angular/toaster-service.md
+++ b/docs/en/framework/ui/angular/toaster-service.md
@@ -4,7 +4,7 @@ You can use the `ToasterService` in @abp/ng.theme.shared package to display mess
## Getting Started
-You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
+You do not have to provide the `ToasterService` at component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ToasterService } from '@abp/ng.theme.shared';
@@ -170,19 +170,19 @@ export class CustomToasterService implements Toaster.Service {
```
```js
-// app.module.ts
+// app.config.ts
import { ToasterService } from '@abp/ng.theme.shared';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
- // ...
+ // ...
{
provide: ToasterService,
useClass: CustomToasterService,
},
- ]
-})
+ ],
+};
```
## API
diff --git a/docs/en/images/angular-folder-structure.png b/docs/en/images/angular-folder-structure.png
index cfc5c6d0e2..f8b6ea656c 100644
Binary files a/docs/en/images/angular-folder-structure.png and b/docs/en/images/angular-folder-structure.png differ
diff --git a/docs/en/images/cmskit-module-page-feedback-widget-2.png b/docs/en/images/cmskit-module-page-feedback-widget-2.png
index 45510eebe0..b2702d9b69 100644
Binary files a/docs/en/images/cmskit-module-page-feedback-widget-2.png and b/docs/en/images/cmskit-module-page-feedback-widget-2.png differ
diff --git a/docs/en/images/cmskit-module-page-feedback-widget.png b/docs/en/images/cmskit-module-page-feedback-widget.png
index cd8264f7eb..b0bf6699ab 100644
Binary files a/docs/en/images/cmskit-module-page-feedback-widget.png and b/docs/en/images/cmskit-module-page-feedback-widget.png differ
diff --git a/docs/en/modules/account-pro.md b/docs/en/modules/account-pro.md
index c53a6f24ec..594d51da62 100644
--- a/docs/en/modules/account-pro.md
+++ b/docs/en/modules/account-pro.md
@@ -301,47 +301,41 @@ See the `AccountPermissions` class members for all permissions defined for this
#### Installation
-In order to configure the application to use the `AccountPublicModule` and the `AccountAdminModule`, you first need to import `AccountPublicConfigModule` from `@volo/abp.ng.account/public/config` and `AccountAdminConfigModule` from `@volo/abp.ng.account/admin/config` to root module. Config modules has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the public account module and the admin account module, you first need to import `provideAccountPublicConfig` from `@volo/abp.ng.account/public/config` and `provideAccountAdminConfig` from `@volo/abp.ng.account/admin/config`. Then, you will need to append them to the `appConfig` array.
```js
-// app.module.ts
-import { AccountAdminConfigModule } from '@volo/abp.ng.account/admin/config';
-import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
-
-@NgModule({
- imports: [
- // other imports
- AccountPublicConfigModule.forRoot(),
- AccountAdminConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideAccountPublicConfig } from '@volo/abp.ng.account/public/config';
+import { provideAccountAdminConfig } from '@volo/abp.ng.account/admin/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideAccountAdminConfig(),
+ provideAccountPublicConfig(),
],
- // ...
-})
-export class AppModule {}
+};
```
-The `AccountPublicModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.account/public`.
+The account public package should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.account/public`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+export const APP_ROUTES: Routes = [
+ // ...
{
path: 'account',
- loadChildren: () =>
- import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy(/* options here */)),
+ loadChildren: () => import('@volo/abp.ng.account/public').then(c => c.createRoutes(/* options here */)),
},
];
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has the modules.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has the necessary configurations.
Options
-You can modify the look and behavior of the module pages by passing the following options to `AccountModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **redirectUrl**: Default redirect URL after logging in.
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
diff --git a/docs/en/modules/audit-logging-pro.md b/docs/en/modules/audit-logging-pro.md
index cb32ff52fe..607d67539c 100644
--- a/docs/en/modules/audit-logging-pro.md
+++ b/docs/en/modules/audit-logging-pro.md
@@ -131,7 +131,12 @@ To see `AbpAuditingOptions` properties, please see its [documentation](../framew
Configure
(options =>
{
options.Period = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
- options.CronExpression = "0 23 * * *"; // This Cron expression only works if Hangfire or Quartz is used for background workers.
+
+ // This Cron expression only works if Hangfire or Quartz is used for background workers.
+ // The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links:
+ // https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions
+ // https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html
+ options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?"
});
```
@@ -147,7 +152,12 @@ Configure(options =>
options.FileRetentionHours = 24; // How long to keep files before cleanup (default: 24 hours)
options.DownloadBaseUrl = "https://yourdomain.com"; // Base URL for download links in emails
options.ExcelFileCleanupOptions.Period = (int)TimeSpan.FromHours(24).TotalMilliseconds; // Interval of the cleanup worker (default: 24 hours)
- options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // This Cron expression only works if Hangfire or Quartz is used for background workers.
+
+ // This Cron expression only works if Hangfire or Quartz is used for background workers.
+ // The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links:
+ // https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions
+ // https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html
+ options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?"
});
```
@@ -234,45 +244,39 @@ See the `AbpAuditLoggingPermissions` class members for all permissions defined f
#### Installation
-In order to configure the application to use the `AuditLoggingModule`, you first need to import `AuditLoggingConfigModule` from `@volo/abp.ng.audit-logging/config` to root module. `AuditLoggingConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the audit logging module, you first need to import `provideAuditLoggingConfig` from `@volo/abp.ng.audit-logging/config` to root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { AuditLoggingConfigModule } from '@volo/abp.ng.audit-logging/config';
-
-@NgModule({
- imports: [
- // other imports
- AuditLoggingConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideAuditLoggingConfig } from '@volo/abp.ng.audit-logging/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideAuditLoggingConfig(),
],
- // ...
-})
-export class AppModule {}
+};
```
-The `AuditLoggingModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.audit-logging`.
+The audit logging module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.audit-logging`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+export const APP_ROUTES: Routes = [
+ // ...
{
path: 'audit-logs',
- loadChildren: () =>
- import('@volo/abp.ng.audit-logging').then(m => m.AuditLoggingModule.forLazy(/* options here */)),
+ loadChildren: () => import('@volo/abp.ng.audit-logging').then(c => c.createRoutes(/* options here */)),
},
];
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `AuditLoggingConfigModule` and `AuditLoggingModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.
Options
-You can modify the look and behavior of the module pages by passing the following options to `AuditLoggingModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/chat.md b/docs/en/modules/chat.md
index 589980ec98..42379b29bc 100644
--- a/docs/en/modules/chat.md
+++ b/docs/en/modules/chat.md
@@ -206,38 +206,33 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
#### Installation
-In order to configure the application to use the `ChatModule`, you first need to import `ChatConfigModule` from `@volo/abp.ng.chat/config` to root module. `ChatConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the chat module, you first need to import `provideChatConfig` from `@volo/abp.ng.chat/config` to root application confiuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { ChatConfigModule } from '@volo/abp.ng.chat/config';
-
-@NgModule({
- imports: [
- // other imports
- ChatConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideChatConfig } from '@volo/abp.ng.chat/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideChatConfig(),
],
- // ...
-})
-export class AppModule {}
+};
+
```
-The `ChatModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. It is available for import from `@volo/abp.ng.chat`.
+The chat module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. It is available for import from `@volo/abp.ng.chat`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'chat',
loadChildren: () =>
- import('@volo/abp.ng.chat').then(m => m.ChatModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.chat').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
#### Services / Models
diff --git a/docs/en/modules/cms-kit-pro/page-feedback.md b/docs/en/modules/cms-kit-pro/page-feedback.md
index e480e024fd..dd519eb15b 100644
--- a/docs/en/modules/cms-kit-pro/page-feedback.md
+++ b/docs/en/modules/cms-kit-pro/page-feedback.md
@@ -2,10 +2,9 @@
> You must have an ABP Team or a higher license to use CMS Kit Pro module's features.
-The CMS Kit provides a **Page Feedback** system to collect feedback from users about pages.
+The CMS Kit Pro module provides a comprehensive **Page Feedback** system that enables you to collect valuable user feedback about your website pages. This system allows visitors to quickly rate their experience and provide comments, helping you understand user satisfaction and identify areas for improvement.
-|  |  |
-| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
+
## Enabling the Page Feedback System
diff --git a/docs/en/modules/cms-kit/index.md b/docs/en/modules/cms-kit/index.md
index 5c30806028..3744ce3590 100644
--- a/docs/en/modules/cms-kit/index.md
+++ b/docs/en/modules/cms-kit/index.md
@@ -72,6 +72,23 @@ CMS kit packages are designed for various usage scenarios. If you check the [CMS
- `Volo.CmsKit.Public.*` packages contain the functionalities used in public websites where users read blog posts or leave comments.
- `Volo.CmsKit.*` (without Admin/Public suffix) packages are called as unified packages. Unified packages are shortcuts for adding Admin & Public packages (of the related layer) separately. If you have a single application for administration and public web site, you can use these packages.
+## Integrating Public and Admin Packages in a Unified Application
+
+If you are using a single application for both admin and public web site, it's important to configure the global layout settings appropriately. By default, the layout is set for a **Public Website**, which is suitable for public-facing pages. However, when your application serves both admin and public pages, you should explicitly set the global layout for all CMS Kit pages.
+
+To do this, add a `_ViewStart.cshtml` file to your web project at `/Pages/Public/CmsKit/_ViewStart.cshtml` and configure the layout as shown below:
+
+```html
+@using Volo.Abp.AspNetCore.Mvc.UI.Theming
+@inject IThemeManager ThemeManager
+@{
+ // default: GetPublicLayout()
+ Layout = ThemeManager.CurrentTheme.GetApplicationLayout();
+}
+```
+
+> The `_ViewStart.cshtml` file is used to set the layout for all pages in the `CmsKit` folder.
+
## Internals
### Table / collection prefix & schema
diff --git a/docs/en/modules/gdpr.md b/docs/en/modules/gdpr.md
index bef763a41a..43e38eb6b2 100644
--- a/docs/en/modules/gdpr.md
+++ b/docs/en/modules/gdpr.md
@@ -177,45 +177,46 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
### Installation
-In order to configure the application to use the `GdprModule`, you first need to import `GdprConfigModule` from `@volo/abp.ng.gdpr/config` to the root module. `GdprConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the gdpr module, you first need to import `provideGdprConfig` from `@volo/abp.ng.gdpr/config` to the root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { GdprConfigModule } from '@volo/abp.ng.gdpr/config';
-
-@NgModule({
- imports: [
- // other imports
- GdprConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import {
+ provideGdprConfig,
+ withCookieConsentOptions,
+} from '@volo/abp.ng.gdpr/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideGdprConfig(
+ withCookieConsentOptions({
+ cookiePolicyUrl: '/gdpr-cookie-consent/cookie',
+ privacyPolicyUrl: '/gdpr-cookie-consent/privacy',
+ }),
+ ),
],
- // ...
-})
-export class AppModule {}
+};
```
-The `GdprModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.gdpr`.
+The gdpr module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.gdpr`.
```js
-// app-routing.module.ts
-const routes: Routes = [
+// app.routes.ts
+const APP_ROUTES: Routes = [
// other route definitions
{
path: 'gdpr',
loadChildren: () =>
- import('@volo/abp.ng.gdpr').then(m => m.GdprModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.gdpr').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `GdprConfigModule` and `GdprModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.
Options
-You can modify the look and behavior of the module pages by passing the following options to the `GdprModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to the `createRoutes` static method:
- **entityActionContributors:** Changes the grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes the page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/identity-pro.md b/docs/en/modules/identity-pro.md
index ab25a8d5c8..6649a335e3 100644
--- a/docs/en/modules/identity-pro.md
+++ b/docs/en/modules/identity-pro.md
@@ -343,45 +343,39 @@ See the `IdentityPermissions` class members for all permissions defined for this
#### Installation
-In order to configure the application to use the `IdentityModule`, you first need to import `IdentityConfigModule` from `@volo/abp.ng.identity/config` to root module. `IdentityConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the identity module, you first need to import `provideIdentityConfig` from `@volo/abp.ng.identity/config` to root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { IdentityConfigModule } from '@volo/abp.ng.identity/config';
-
-@NgModule({
- imports: [
- // other imports
- IdentityConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideIdentityConfig } from '@volo/abp.ng.identity/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideIdentityConfig(),
],
- // ...
-})
-export class AppModule {}
+};
```
-The `IdentityModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity`.
+The identity module should be imported and lazy-loaded in your routing configuration. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'identity',
loadChildren: () =>
- import('@volo/abp.ng.identity').then(m => m.IdentityModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.identity').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `IdentityConfigModule` and `IdentityModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations added.
Options
-You can modify the look and behavior of the module pages by passing the following options to `IdentityModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/identity-server-pro.md b/docs/en/modules/identity-server-pro.md
index ce25823ee8..ad197e8a7d 100644
--- a/docs/en/modules/identity-server-pro.md
+++ b/docs/en/modules/identity-server-pro.md
@@ -245,45 +245,39 @@ See the `AbpIdentityServerPermissions` class members for all permissions defined
#### Installation
-In order to configure the application to use the `IdentityServerModule`, you first need to import `IdentityServerConfigModule` from `@volo/abp.ng.identity-server/config` to root module. `IdentityServerConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the identity server, you first need to import `provideIdentityServerConfig` from `@volo/abp.ng.identity-server/config` to root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { IdentityServerConfigModule } from '@volo/abp.ng.identity-server/config';
-
-@NgModule({
- imports: [
- // other imports
- IdentityServerConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideIdentityServerConfig } from '@volo/abp.ng.identity-server/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideIdentityServerConfig()
],
- // ...
-})
-export class AppModule {}
+};
```
-The `IdentityServerModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity-server`.
+The identity server module should be imported and lazy-loaded in your routing module. It has a static `creatRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity-server`.
```js
-// app-routing.module.ts
-const routes: Routes = [
+// app.routes.ts
+const APP_ROUTES: Routes = [
// other route definitions
{
path: 'identity-server',
loadChildren: () =>
- import('@volo/abp.ng.identity-server').then(m => m.IdentityServerModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.identity-server').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `IdentityServerConfigModule` and `IdentityServerModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.
Options
-You can modify the look and behavior of the module pages by passing the following options to `IdentityServerModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/language-management.md b/docs/en/modules/language-management.md
index 62327378a4..536abc7ab3 100644
--- a/docs/en/modules/language-management.md
+++ b/docs/en/modules/language-management.md
@@ -149,45 +149,41 @@ See the `LanguageManagementPermissions` class members for all permissions define
#### Installation
-To configure the application to use the `LanguageManagementModule`, you first need to import `LanguageManagementConfigModule` from `@volo/abp.ng.language-management/config` to root module. `LanguageManagementConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+To configure the application to use the language management module, you first need to import `provideLanguageManagementConfig` from `@volo/abp.ng.language-management/config` to root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { LanguageManagementConfigModule } from '@volo/abp.ng.language-management/config';
-
-@NgModule({
- imports: [
- // other imports
- LanguageManagementConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideLanguageManagementConfig } from '@volo/abp.ng.language-management/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideLanguageManagementConfig()
],
- // ...
-})
-export class AppModule {}
+};
+
```
-The `LanguageManagementModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.language-management`.
+The language management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.language-management`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'language-management',
loadChildren: () =>
- import('@volo/abp.ng.language-management').then(m => m.LanguageManagementModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.language-management').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything because it already has both `LanguageManagementConfigModule` and `LanguageManagementModule`.
+> If you have generated your project via the startup template, you do not have to do anything because it already has both configurations implemented.
Options
-You can modify the look and behavior of the module pages by passing the following options to `LanguageManagementModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/payment.md b/docs/en/modules/payment.md
index b3fb6de84b..6a55cc83ae 100644
--- a/docs/en/modules/payment.md
+++ b/docs/en/modules/payment.md
@@ -81,38 +81,36 @@ This page is used to send Name, Surname and Email Address of user to PayU.
#### Installation
-In order to configure the application to use the `PaymentModule`, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root module. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration:
+In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration:
```js
-// app.module.ts
+// app.config.ts
+import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { PaymentAdminConfigModule } from '@volo/abp.ng.payment/admin/config';
-@NgModule({
- imports: [
- // other imports
- PaymentAdminConfigModule.forRoot(),
- // other imports
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ importProvidersFrom([
+ PaymentAdminConfigModule.forRoot()
+ ]),
],
- // ...
-})
-export class AppModule {}
+};
+
```
-The `PaymentAdminModule` should be imported and lazy-loaded in your routing module as below:
+The payment admin module should be imported and lazy-loaded in your routing array as below:
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'payment',
loadChildren: () =>
- import('@volo/abp.ng.payment/admin').then(m => m.PaymentAdminModule.forLazy()),
+ import('@volo/abp.ng.payment/admin').then(c => c.createRoutes()),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
#### Payment plans page
diff --git a/docs/en/modules/saas.md b/docs/en/modules/saas.md
index bfd0115853..f1e85d6ceb 100644
--- a/docs/en/modules/saas.md
+++ b/docs/en/modules/saas.md
@@ -244,45 +244,39 @@ See the `SaasHostPermissions` class members for all permissions defined for this
#### Installation
-In order to configure the application to use the `SaasModule`, you first need to import `SaasConfigModule` from `@volo/abp.ng.saas/config` to root module. `SaasConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the saas module, you first need to import `provideSaasConfig` from `@volo/abp.ng.saas/config` to root module. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { SaasConfigModule } from '@volo/abp.ng.saas/config';
-
-@NgModule({
- imports: [
- // other imports
- SaasConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideSaasConfig } from '@volo/abp.ng.saas/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideSaasConfig(),
],
- // ...
-})
-export class AppModule {}
+};
```
-The `SaasModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.saas`.
+The saas module should be imported and lazy-loaded in your routing configuration. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.saas`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'saas',
loadChildren: () =>
- import('@volo/abp.ng.saas').then(m => m.SaasModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.saas').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `SaasConfigModule` and `SaasModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations implemented.
Options
-You can modify the look and behavior of the module pages by passing the following options to `SaasModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md
index 9b2235314f..7f045cd8e1 100644
--- a/docs/en/modules/text-template-management.md
+++ b/docs/en/modules/text-template-management.md
@@ -162,45 +162,39 @@ See the `TextTemplateManagementPermissions` class members for all permissions de
#### Installation
-In order to configure the application to use the `TextTemplateManagementModule`, you first need to import `TextTemplateManagementConfigModule` from `@volo/abp.ng.text-template-management/config` to root module. `TextTemplateManagementConfigModule` has a static `forRoot` method which you should call for a proper configuration.
+In order to configure the application to use the text template management module, you first need to import `provideTextTemplateManagementConfig` from `@volo/abp.ng.text-template-management/config` to root configuration. Then, you will need to append it to the `appConfig` array.
```js
-// app.module.ts
-import { TextTemplateManagementConfigModule } from '@volo/abp.ng.text-template-management/config';
-
-@NgModule({
- imports: [
- // other imports
- TextTemplateManagementConfigModule.forRoot(),
- // other imports
+// app.config.ts
+import { provideTextTemplateManagementConfig } from '@volo/abp.ng.text-template-management/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideTextTemplateManagementConfig()
],
- // ...
-})
-export class AppModule {}
+};
```
-The `TextTemplateManagementModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.text-template-management`.
+The text template management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.text-template-management`.
```js
-// app-routing.module.ts
-const routes: Routes = [
- // other route definitions
+// app.routes.ts
+const APP_ROUTES: Routes = [
+ // ...
{
path: 'text-template-management',
loadChildren: () =>
- import('@volo/abp.ng.text-template-management').then(m => m.TextTemplateManagementModule.forLazy(/* options here */)),
+ import('@volo/abp.ng.text-template-management').then(c => c.createRoutes(/* options here */)),
},
];
-
-@NgModule(/* AppRoutingModule metadata */)
-export class AppRoutingModule {}
```
-> If you have generated your project via the startup template, you do not have to do anything, because it already has both `TextTemplateManagementConfigModule` and `TextTemplateManagementModule`.
+> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations implemented.
Options
-You can modify the look and behavior of the module pages by passing the following options to `TextTemplateManagementModule.forLazy` static method:
+You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
diff --git a/docs/en/release-info/migration-guides/abp-9-3.md b/docs/en/release-info/migration-guides/abp-9-3.md
index 0de650501e..7da446beee 100644
--- a/docs/en/release-info/migration-guides/abp-9-3.md
+++ b/docs/en/release-info/migration-guides/abp-9-3.md
@@ -2,6 +2,14 @@
This document is a guide for upgrading ABP v9.2 solutions to ABP v9.3. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application.
+## Switched to `MySql.EntityFrameworkCore` for EF Core MySQL Provider
+
+In this version, we switched the EF Core MySQL provider from `Pomelo.EntityFrameworkCore.MySql` to `MySql.EntityFrameworkCore`.
+
+If you want to use the `Pomelo.EntityFrameworkCore.MySql` provider, then you can follow the [Use Pomelo provider documentation](https://abp.io/docs/latest/framework/data/entity-framework-core/mysql#use-pomelo-provider) to migrate your application.
+
+> See the internal changes we made in [#23392](https://github.com/abpframework/abp/pull/23392), for implementation details.
+
## Updated `RabbitMQ.Client` to `7.x`
In this version, we updated `RabbitMQ.Client` to `7.1.2`. [This is a major version update](https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/main/v7-MIGRATION.md) that brings significant improvements to the library:
diff --git a/docs/en/solution-templates/application-module/index.md b/docs/en/solution-templates/application-module/index.md
index 2b6de263fc..48013d2d90 100644
--- a/docs/en/solution-templates/application-module/index.md
+++ b/docs/en/solution-templates/application-module/index.md
@@ -183,45 +183,29 @@ The issue management page is empty in the beginning. You may change the content
Now, let's have a closer look at some key elements of your project.
-### The Main Module
+### The Main Component
-`IssueManagementModule` at the _angular/projects/issue-management/src/lib/issue-management.module.ts_ path is the main module of your module project. There are a few things worth mentioning in it:
+`IssueManagementComponent` at the _angular/projects/issue-management/src/lib/issue-management.routes.ts_ path is the main component of your module project. There are a few things worth mentioning in it:
-- Essential ABP modules, i.e. `CoreModule` and `ThemeSharedModule`, are imported.
-- `IssueManagementRoutingModule` is imported.
-- `IssueManagementComponent` is declared.
-- It is prepared for configurability. The `forLazy` static method enables [a configuration to be passed to the module when it is loaded by the router](https://volosoft.com/blog/how-to-configure-angular-modules-loaded-by-the-router).
-
-
-### The Main Routing Module
-
-`IssueManagementRoutingModule` at the _angular/projects/issue-management/src/lib/issue-management-routing.module.ts_ path is the main routing module of your module project. It currently does two things:
-
-- Loads `DynamicLayoutComponent` at base path it is given.
-- Loads `IssueManagementComponent` as child to the layout, again at the given base path.
-
-You can rearrange this module to load more than one component at different routes, but you need to update the route provider at _angular/projects/issue-management/config/src/providers/route.provider.ts_ to match the new routing structure with the routes in the menu. Please check [Modifying the Menu](../../framework/ui/angular/modifying-the-menu.md) to see how route providers work.
+- `IssueManagementComponent` is declared as standalone within the latest migration.
+- `ISSUE_MANAGEMENT_ROUTES` is configured to be lazy-loaded.
### The Config Module
-There is a config module at the _angular/projects/issue-management/config/src/issue-management-config.module.ts_ path. The static `forRoot` method of this module is supposed to be called at the route level. So, you may assume the following will take place:
+There is a config module at the _angular/projects/issue-management/config/src/providers/route.provider.ts_ path. The static `provideIssueManagement` method of this module is supposed to be called at the route level. So, you may assume the following will take place:
```js
-@NgModule({
- imports: [
- /* other imports */
-
- IssueManagementConfigModule.forRoot(),
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideIssueManagement(),
+ // ...
],
-
- /* rest of the module meta data */
-})
-export class AppModule {}
+};
```
You can use this static method to configure an application that uses your module project. An example of such configuration is already implemented and the `ISSUE_MANAGEMENT_ROUTE_PROVIDERS` token is provided here. The method can take options which enables further configuration possibilities.
-The difference between the `forRoot` method of the config module and the `forLazy` method of the main module is that, for smallest bundle size, the former should only be used when you have to configure an app before your module is even loaded.
### Testing Angular UI
diff --git a/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png
index 95bfc8986f..1ee4f87081 100644
Binary files a/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png and b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png differ
diff --git a/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png
index dd7a4e5cc7..10841cccd0 100644
Binary files a/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png and b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png differ
diff --git a/docs/en/solution-templates/layered-web-application/web-applications.md b/docs/en/solution-templates/layered-web-application/web-applications.md
index 8a1b71fda7..ce67866815 100644
--- a/docs/en/solution-templates/layered-web-application/web-applications.md
+++ b/docs/en/solution-templates/layered-web-application/web-applications.md
@@ -49,25 +49,25 @@ The Angular application runs as a client-side SPA in the user's browser and comm

-Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
+Each of ABP module is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules are installed with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
```bash
yarn list --pattern abp
```
-Angular application module structure:
+Angular application structure:

-### AppModule
+### Application Config
-`AppModule` is the root module of the application. Some of the ABP modules and some essential modules are imported to `AppModule`.
+Application config is the root configuration of the application. Some of the ABP modules and some essential providers are imported to `appConfig`.
-ABP Config modules have also been imported to `AppModule`Â for initial requirements of the lazy-loadable ABP modules.
+ABP Config modules have also been provided in `appConfig`Â for initial requirements of the lazy-loadable ABP modules.
-### AppRoutingModule
+### APP_ROUTES
-There are lazy-loadable ABP modules in the `AppRoutingModule` as routes.
+There are lazy-loadable ABP modules in the `APP_ROUTES` as routes.
> Paths of ABP Modules should not be changed.
@@ -76,7 +76,7 @@ You should add `routes` property in the `data` object to add a link on the menu
```js
{
path: 'dashboard',
- loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
+ loadComponent: () => import('./dashboard/dashboard.component').then(c => c.DashboardComponent),
canActivate: [authGuard, permissionGuard],
data: {
routes: {
@@ -97,19 +97,13 @@ In the above example;
After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu.
-### Shared Module
-
-The modules that may be required for all modules have been imported to the `SharedModule`. You should import `SharedModule` to all modules.
-
-See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document.
-
### Environments
The files under the `src/environments` folder have the essential configuration of the application.
-### Home Module
+### Home Component
-Home module is an example lazy-loadable module that loads on the root address of the application.
+Home component is an example lazy-loadable component that loads on the root address of the application.
### Styles
diff --git a/docs/en/solution-templates/microservice/localization-system.md b/docs/en/solution-templates/microservice/localization-system.md
index a350de6848..e11232236e 100644
--- a/docs/en/solution-templates/microservice/localization-system.md
+++ b/docs/en/solution-templates/microservice/localization-system.md
@@ -92,12 +92,12 @@ You can define new localization entries in the language files under the **Locali
Angular UI gets the localization resources from the [`application-localization`](../../framework/api-development/standard-apis/localization.md) API's response and merges these resources in the `ConfigStateService` for the localization entries/resources coming from the backend side.
-In addition, you may need to define some localization entries and only use them on the UI side. ABP already provides the related configuration for you, so you don't need to make any configurations related to that and instead you can directly define localization entries in the `app.-module.ts` file of your angular application as follows:
+In addition, you may need to define some localization entries and only use them on the UI side. ABP already provides the related configuration for you, so you don't need to make any configurations related to that and instead you can directly define localization entries in the `app.config.ts` file of your angular application as follows:
```ts
import { provideAbpCore, withOptions } from '@abp/ng.core';
-@NgModule({
+export const appConfig: ApplicationConfig = {
providers: [
// ...
provideAbpCore(
@@ -119,11 +119,8 @@ import { provideAbpCore, withOptions } from '@abp/ng.core';
]
}),
),
- ...
],
-})
-export class AppModule {}
-
+};
```
After defining the localization entries, it can be used as below:
diff --git a/docs/en/suite/solution-structure.md b/docs/en/suite/solution-structure.md
index b1949a137a..5abd50d751 100644
--- a/docs/en/suite/solution-structure.md
+++ b/docs/en/suite/solution-structure.md
@@ -226,25 +226,21 @@ Angular application folder structure looks like below:

-Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
+Each of ABP module is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules are installed with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
```bash
yarn list --pattern abp
```
-Angular application module structure:
+### Application Config
-
+Application config is the root module of the application. Some of ABP modules and some essential modules are imported to the `appConfig`.
-### AppModule
+ABP Config modules also have been imported to `appConfig`Â for initially requirements of lazy-loadable ABP modules.
-`AppModule` is the root module of the application. Some of ABP modules and some essential modules imported to the `AppModule`.
+### APP_ROUTES
-ABP Config modules also have imported to `AppModule`Â for initially requirements of lazy-loadable ABP modules.
-
-### AppRoutingModule
-
-There are lazy-loadable ABP modules in the `AppRoutingModule` as routes.
+There are lazy-loadable ABP modules in the `APP_ROUTES` as routes.
> Paths of ABP Modules should not be changed.
@@ -253,7 +249,7 @@ You should add `routes` property in the `data` object to add a link on the menu
```js
{
path: 'dashboard',
- loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
+ loadComponent: () => import('./dashboard/dashboard.component').then(c => c.DashboardComponent),
canActivate: [authGuard, permissionGuard],
data: {
routes: {
@@ -274,25 +270,19 @@ In the above example;
After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu.
-### Shared Module
-
-The modules that may be required for all modules have imported to the `SharedModule`. You should import the `SharedModule` to all modules.
-
-See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document.
-
### Environments
The files under the `src/environments` folder has the essential configuration of the application.
-### Home Module
+### Home Component
-Home module is an example lazy-loadable module that loads on the root address of the application.
+Home component is an example lazy-loadable standalone component that loads on the root address of the application.
-### Dashboard Module
+### Dashboard Component
-Dashboard module is a lazy-loadable module. `HostDashboardComponent` and `TenantDashboardComponent` declared to this module. One of these components is shown according to the user's authorization.
+Dashboard component is a lazy-loadable component. `HostDashboardComponent` and `TenantDashboardComponent` are declared within this root component. One of these components is shown according to authorization of the user.
-There are four widgets in the `HostDashboardComponent` which declared in ABP modules.
+There are four widgets in the `HostDashboardComponent` which are declared in ABP modules.
### Styles
diff --git a/docs/en/tutorials/microservice/part-05.md b/docs/en/tutorials/microservice/part-05.md
index 3fcfc9cac8..9c037617a1 100644
--- a/docs/en/tutorials/microservice/part-05.md
+++ b/docs/en/tutorials/microservice/part-05.md
@@ -516,14 +516,6 @@ abp generate-proxy -t ng -m ordering -u http://localhost:44311 --target ordering
For more information, please refer to the [Service Proxies](https://abp.io/docs/latest/framework/ui/angular/service-proxies) documentation.
-### Create Order Module
-
-Run the following command line to create a new module, named `OrderModule` in the root folder of the angular application:
-
-```bash
-yarn ng generate module order --module ordering-service --project ordering-service --routing --route orders
-```
-
### Add Order Route
* Create `order-base.routes.ts` file under the `projects/ordering-service/config/src/providers` folder and add the following code:
@@ -565,39 +557,102 @@ function configureRoutes() {
routesService.add(routes);
}
```
+* Open the `projects/ordering-service/config/src/providers/route.provider.ts` file and add `ORDERS_ORDER_ROUTE_PROVIDER` to the `ORDER_SERVICE_PROVIDERS` array as following code:
-* Open the `projects/ordering-service/config/src/ordering-service-config.module.ts` file and add `ORDERS_ORDER_ROUTE_PROVIDER` to the `providers` array as following code:
-
-*ordering-service-config.module.ts*
+*route.provider.ts*
```typescript
-import { ModuleWithProviders, NgModule } from '@angular/core';
-import { ORDERING_SERVICE_ROUTE_PROVIDERS } from './providers/route.provider';
-import { ORDERS_ORDER_ROUTE_PROVIDER } from './providers/order-route.provider';
-
-@NgModule()
-export class OrderingServiceConfigModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: OrderingServiceConfigModule,
- providers: [ORDERING_SERVICE_ROUTE_PROVIDERS, ORDERS_ORDER_ROUTE_PROVIDER],
- };
- }
+import { eLayoutType, RoutesService } from '@abp/ng.core';
+import {
+ EnvironmentProviders,
+ inject,
+ makeEnvironmentProviders,
+ provideAppInitializer,
+} from '@angular/core';
+import { eOrderingServiceRouteNames } from '../enums/route-names';
+import { ORDERS_ORDER_ROUTE_PROVIDER } from './order-route.provider';
+
+
+export const ORDER_SERVICE_ROUTE_PROVIDERS = [
+ provideAppInitializer(() => {
+ configureRoutes();
+ }),
+];
+
+export function configureRoutes() {
+ const routesService = inject(RoutesService);
+ routesService.add([
+ {
+ path: '/order-service',
+ name: eOrderingServiceRouteNames.OrderService,
+ iconClass: 'fas fa-book',
+ layout: eLayoutType.application,
+ order: 3,
+ },
+ ]);
+}
+
+const ORDER_SERVICE_PROVIDERS: EnvironmentProviders[] = [
+ ...ORDER_SERVICE_ROUTE_PROVIDERS,
+ ...ORDERS_ORDER_ROUTE_PROVIDER
+];
+
+export function provideOrderService() {
+ return makeEnvironmentProviders(ORDER_SERVICE_PROVIDERS);
}
```
+* Do not forget adding `provideOrderService()` to the providers inside `app.config.ts` as follows:
+
+```typescript
+import { provideOrderService } from '@order-service/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideOrderService()
+ ],
+};
+```
+
+* Lastly, you need to update the `APP_ROUTES` array in `app.routes.ts` file as follows:
+
+```typescript
+// app.routes.ts
+export const APP_ROUTES: Routes = [
+ // ...
+ {
+ path: 'order-service',
+ children: ORDER_SERVICE_ROUTES,
+ },
+];
+```
+
+```typescript
+// order-service.routes.ts
+export const ORDER_SERVICE_ROUTES: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: RouterOutletComponent,
+ },
+ { path: 'orders', children: ORDER_ROUTES },
+];
+```
+
### Create Order Page
* Create `order.component.ts` file under the `projects/ordering-service/src/lib/order` folder as following code:
```typescript
import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
import { OrderDto, OrderService } from './proxy/ordering-service/services';
@Component({
selector: 'lib-order',
- standalone: false,
templateUrl: './order.component.html',
styleUrl: './order.component.css'
+ imports: [CommonModule]
})
export class OrderComponent {
diff --git a/docs/en/ui-themes/lepton-x-lite/angular.md b/docs/en/ui-themes/lepton-x-lite/angular.md
index 430bc2ee73..2943a78524 100644
--- a/docs/en/ui-themes/lepton-x-lite/angular.md
+++ b/docs/en/ui-themes/lepton-x-lite/angular.md
@@ -31,42 +31,33 @@ yarn add bootstrap-icons
Note: You should remove the old theme styles from "angular.json" if you are switching from "ThemeBasic" or "Lepton."
Look at the [Theme Configurations](../../framework/ui/angular/theme-configurations.md) list of styles. Depending on your theme, you can alter your styles in angular.json.
-- Finally, remove `ThemeBasicModule`, `provideThemeBasicConfig` from `app.module.ts`, and import the related modules in `app.module.ts`
+- Finally, remove `provideThemeBasicConfig` from `app.config.ts`, and import the related providers in `app.config.ts`
```js
-import { ThemeLeptonXModule } from "@abp/ng.theme.lepton-x";
+import { provideThemeLeptonX } from "@abp/ng.theme.lepton-x";
+import { provideSideMenuLayout } from "@abp/ng.theme.lepton-x/layouts";
-@NgModule({
- imports: [
- // ...
- // do not forget to remove ThemeBasicModule or other old theme module
- // ThemeBasicModule
- ThemeLeptonXModule.forRoot()
- ],
+export const appConfig: ApplicationConfig = {
providers: [
- // do not forget to remove provideThemeBasicConfig or other old theme providers
- // provideThemeBasicConfig
+ // ...
+ provideSideMenuLayout(),
+ provideThemeLeptonX(),
],
- // ...
-})
-export class AppModule {}
+};
```
-Note: If you employ [Resource Owner Password Flow](../../framework/ui/angular/authorization.md#resource-owner-password-flow) for authorization, you should import the following module as well:
+Note: If you employ [Resource Owner Password Flow](../../framework/ui/angular/authorization.md#resource-owner-password-flow) for authorization, you should provide the following provider as well:
```js
-import { AccountLayoutModule } from "@abp/ng.theme.lepton-x/account";
+import { provideAccountLayout } from "@abp/ng.theme.lepton-x/account";
-@NgModule({
- // ...
- imports: [
- // ...
- AccountLayoutModule.forRoot(),
+export const appConfig: ApplicationConfig = {
+ providers: [
// ...
+ provideAccountLayout()
],
- // ...
-})
-export class AppModule {}
+};
+
```
To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss`
diff --git a/docs/en/ui-themes/lepton-x/angular.md b/docs/en/ui-themes/lepton-x/angular.md
index 60ad62b5d9..af1713cec8 100644
--- a/docs/en/ui-themes/lepton-x/angular.md
+++ b/docs/en/ui-themes/lepton-x/angular.md
@@ -9,51 +9,36 @@ To add `LeptonX` into your existing projects, follow the steps below.
Add theme-specific styles into the `styles` array of the file. Check the [Theme Configurations](../../framework/ui/angular/theme-configurations.md#lepton-x-commercial) documentation for more information.
-Importing a CSS file as an ECMA module is not supported in Angular 14. Therefore, we need to add the styles in the angular.json file.
-- At last, remove `ThemeLeptonModule` from `app.module.ts` and `shared.module.ts`, and import the following modules in `app.module.ts`
+- At last, remove `provideThemeLepton` from `app.config.ts`, and add the following providers in `app.config.ts`
+
```ts
-import {
- HttpErrorComponent,
- ThemeLeptonXModule,
-} from "@volosoft/abp.ng.theme.lepton-x";
-import { SideMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts";
-
-@NgModule({
- // ...
- imports: [
- // ...
- // ThemeLeptonModule.forRoot(), -> remove this line.
- ThemeLeptonXModule.forRoot(),
- SideMenuLayoutModule.forRoot(), // depends on which layout you choose
- // ...
+import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x';
+import { provideSideMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts';
+// import { provideThemeLepton } from '@volo/abp.ng.theme.lepton';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // provideThemeLepton() delete this
+ provideSideMenuLayout(), // depends on which layout you choose
+ provideThemeLeptonX(),
],
- // ...
-})
-export class AppModule {}
+};
```
-If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add TopMenuLayoutModule as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial)
+If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add `provideTopMenuLayout` as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial)
```ts
-import {
- HttpErrorComponent,
- ThemeLeptonXModule,
-} from "@volosoft/abp.ng.theme.lepton-x";
-import { TopMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts";
-
-@NgModule({
- // ...
- imports: [
- // ...
- // ThemeLeptonModule.forRoot(), -> remove this line.
- ThemeLeptonXModule.forRoot(),
- TopMenuLayoutModule.forRoot(),
+import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x';
+import { provideTopMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideTopMenuLayout(),
+ provideThemeLeptonX(),
],
- // ...
-})
-export class AppModule {}
+};
```
- At this point, `LeptonX` theme should be up and running within your application. However, you may need to overwrite some css variables based your needs for every theme available as follows:
diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md
index afea2fd228..dc858715d0 100644
--- a/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md
+++ b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md
@@ -37,26 +37,20 @@ export const NEW_THEME_PROVIDER = [
In this code snippet, we create a new instance of the LpxTheme class called myNewThemeDefinition. We specify the bundles that make up the theme (e.g., "bootstrap-custom" and "custom-theme"), the style name for the theme, a label to display in the UI, and an icon (using Bootstrap icons in this example).
### Step 2: Registering the New Theme Provider
-Now that we have defined the new theme appearance, we need to register it as a provider in our Angular project. Open the app.module.ts file (or the module where LeptonX is configured), and add the following code:
+Now that we have defined the new theme appearance, we need to register it as a provider in our Angular project. Open the `app.config.ts` file (or the configuration where LeptonX is configured), and add the following code:
```js
import { NEW_THEME_PROVIDER } from './new-theme.provider.ts';
-@NgModule({
- imports: [
- // ...
- ],
+export const appConfig: ApplicationConfig = {
providers: [
// ...
- NEW_THEME_PROVIDER,
+ NEW_THEME_PROVIDER
],
- // ...
-})
-export class AppModule { }
-
+};
```
-By importing the `NEW_THEME_PROVIDER` from the file where we defined our theme, we can add it to the providers array of our Angular module. This makes the new theme appearance available throughout the application.
+By importing the `NEW_THEME_PROVIDER` from the file where we defined our theme, we can add it to the providers array of our Angular application configuration. This makes the new theme appearance available throughout the application.
### Step 3: Adding the Styles Path to angular.json
diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md
index 4b58c4b106..264091b561 100644
--- a/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md
+++ b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md
@@ -1,26 +1,25 @@
# Configuring the Default Theme for LeptonX
-The LeptonX theme offers multiple appearances to suit your application's visual style. You can easily configure the default theme for your application using the ThemeLeptonXModule provided by LeptonX.
+The LeptonX theme offers multiple appearances to suit your application's visual style. You can easily configure the default theme for your application using the `provideThemeLeptonX` provided by LeptonX.
### Configuration Code
-To set the default theme, you need to configure the ThemeLeptonXModule using the forRoot() function in your application's main module (often referred to as AppModule). Here's an example:
+To set the default theme, you need to configure the `provideThemeLeptonX` using the `withThemeLeptonXOptions({...})` function in the main configuration of your application (often referred to as appConfig). Here's an example:
+
```js
-import { ThemeLeptonXModule } from 'leptonx'; // Import the LeptonX theme module
+import { provideThemeLeptonX, withThemeLeptonXOptions } from '@volosoft/abp.ng.theme.lepton-x';
-@NgModule({
- // ... Other module configurations
- imports: [
- // ... Other imported modules
- ThemeLeptonXModule.forRoot({
- defaultTheme: 'light', // Set the default theme to 'light'
- }),
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideThemeLeptonX(
+ withThemeLeptonXOptions({
+ defaultTheme: 'light'
+ })
+ ),
],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
+};
```
-
-In the example above, we've imported the ThemeLeptonXModule and configured it using the forRoot() function. By providing the defaultTheme parameter and setting its value to 'light',
+In the example above, we've imported the `provideThemeLeptonX` and `withThemeLeptonXOptions`, then configured it using the option parameters. By providing the `defaultTheme` parameter and setting its value to 'light'.
If you delete the defaultTheme parameter in the configuration object, the LeptonX theme will use the default value of "System" as the default theme appearance.
diff --git a/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md
index f68f3bd90f..57cdcd0ec9 100644
--- a/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md
+++ b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md
@@ -5,25 +5,22 @@ First, The custom layout component should be created and implemented for the Ang
Related content can be found in the [Component Replacement Document](../../framework/ui/angular/component-replacement.md#how-to-replace-a-layout)
-
-After creating a custom layout, these imports should be imported in the `app.module.ts` file because the modules contain definitions of the Lepton X components.
+After creating a custom layout, these imports should be imported in the `app.config.ts` file because the modules contain definitions of the Lepton X components.
```javascript
-// app.module.ts
+// app.config.ts
import { LpxSideMenuLayoutModule } from '@volosoft/ngx-lepton-x/layouts';
import { LpxResponsiveModule } from '@volo/ngx-lepton-x.core';// optional. Only, if you are using lpxResponsive directive
- @NgModule({
- //... removed for clearity
- imports: [
- //... removed for clearity
- LpxSideMenuLayoutModule,
- LpxResponsiveModule // <-- Optional
- ]
-})
-export class AppModule {}
-
+export const appConfig: ApplicationConfig = {
+ providers: [
+ importProvidersFrom([
+ LpxSideMenuLayoutModule,
+ LpxResponsiveModule // <-- Optional
+ ])
+ ],
+};
```
Here is the simplified version of the `side-menu-layout.ts` file. Only the ABP Component Replacement code has been removed.
diff --git a/docs/en/ui-themes/lepton/customizing-lepton-theme.md b/docs/en/ui-themes/lepton/customizing-lepton-theme.md
index fb88742c5b..d626fd2769 100644
--- a/docs/en/ui-themes/lepton/customizing-lepton-theme.md
+++ b/docs/en/ui-themes/lepton/customizing-lepton-theme.md
@@ -12,19 +12,28 @@ You may want to change certain aspects of your website’s appearance with a cu
## Adding Custom Style
-There is a `customStyle` boolean configuration in `ThemeLeptonModule`'s `forRoot` method. If this configuration is true, the style selection box is not included in the theme settings form and `ThemeLeptonModule` does not load its own styles. In this case, a custom style file must be added to the styles array in `angular.json` or must be imported by `style.scss`.
+There is a `customStyle` boolean configuration in `provideThemeLepton(withLeptonOptions({...}))` method. If this configuration is true, the style selection box is not included in the theme settings form and `theme-lepton` does not load its own styles. In this case, a custom style file must be added to the styles array in `angular.json` or must be imported by `style.scss`.
> Only angular project styles can be changed in this way. If the authorization flow is authorization code flow, MVC pages (login, profile, etc) are not affected by this change.
Custom style implementation can be done with the following steps
-Set `customStyle` property to `true` where is `ThemeLeptonModule` imported with `forRoot` method.
+Set `customStyle` property to `true` where `provideThemeLepton(withLeptonOptions({...}))` method is called.
```javascript
-// app.module.ts
-ThemeLeptonModule.forRoot({
- customStyle: true
-})
+// app.config.ts
+import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideThemeLepton(
+ withLeptonOptions({
+ customStyle: true
+ })
+ )
+ ],
+};
```
Import your style file to `src/style.scss`
@@ -68,7 +77,7 @@ Or add your style file to the `styles` arrays which in `angular.json` file
## Inserting Custom Content To Lepton Menu
-Lepton menu can take custom content both before and after the menu items displayed. In order to achieve this, pass a component as content through the parameters of `ThemeLeptonModule.forRoot` when you import the module in your root module, i.e. `AppModule`. Let's take a look at some examples.
+Lepton menu can take custom content both before and after the menu items displayed. In order to achieve this, pass a component as content through the parameters of `provideThemeLepton(withLeptonOptions({...}))` when you import the provider in your root app configuration, i.e. `appConfig`. Let's take a look at some examples.
### Placing Custom Content Before & After Menu Items
@@ -76,7 +85,10 @@ Lepton menu can take custom content both before and after the menu items display
First step is to create a component which will serve as the custom content.
```js
+// ...
@Component({
+ // ...
+ imports: [AsyncPipe],
template: `
Support Issues
@@ -88,27 +100,23 @@ First step is to create a component which will serve as the custom content.
export class SupportLinkComponent {
issueCount$ = of(26); // dummy count, replace this with an actual service
}
-
-@NgModule({
- declarations: [SupportLinkComponent],
- imports: [CommonModule],
-})
-export class SupportLinkModule {}
```
-Now, pass this component as `contentAfterRoutes` option to `ThemeLeptonModule`.
+Now, pass this component as `contentAfterRoutes` option to `provideThemeLepton(withLeptonOptions({...}))`.
```js
-@NgModule({
- imports: [
- // other imports are removed for sake of brevity
- SupportLinkModule,
- ThemeLeptonModule.forRoot({
- contentAfterRoutes: [SupportLinkComponent],
- })
+import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideThemeLepton(
+ withLeptonOptions({
+ contentAfterRoutes: [SupportLinkComponent],
+ })
+ )
],
-})
-export class AppModule {}
+};
```
If you start the dev server, you must see the inserted content as follows:
@@ -124,24 +132,25 @@ Placing the content before menu items is straightforward: Just replace `contentA
### Placing a Search Input Before Menu Items
-The Lepton package has a search component designed to work with the routes in the menu. You can simply import the module and pass the component as `contentBeforeRoutes` option to `ThemeLeptonModule`.
+The Lepton package has a search component designed to work with the routes in the menu. You can simply import the provider and pass the component as `contentBeforeRoutes` option to `provideThemeLepton(withLeptonOptions({...}))`.
```js
-import { MenuSearchComponent, MenuSearchModule } from '@volo/abp.ng.theme.lepton/extensions';
-
-@NgModule({
- imports: [
- // other imports are removed for sake of brevity
-
- MenuSearchModule.forRoot({
+import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton';
+import { MenuSearchComponent, provideMenuSearch } from '@volo/abp.ng.theme.lepton/extensions';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ // ...
+ provideThemeLepton(
+ withLeptonOptions({
+ contentBeforeRoutes: [MenuSearchComponent],
+ })
+ ),
+ provideMenuSearch({
limit: 3 // search result limit (default: Infinity)
- }),
- ThemeLeptonModule.forRoot({
- contentBeforeRoutes: [MenuSearchComponent],
})
],
-})
-export class AppModule {}
+};
```
Here is how the search input works:
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs
new file mode 100644
index 0000000000..fa95f5c298
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/TelemetryApplicationMetricsEnricher.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Services;
+using Volo.Abp.DependencyInjection;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.Internal.Telemetry.Activity;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.AspNetCore.Mvc;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+public sealed class TelemetryApplicationMetricsEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly ITypeFinder _typeFinder;
+ public TelemetryApplicationMetricsEnricher(ITypeFinder typeFinder, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _typeFinder = typeFinder;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime);
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ var appServiceCount = _typeFinder.Types.Count(t =>
+ typeof(IApplicationService).IsAssignableFrom(t) &&
+ t is { IsAbstract: false, IsInterface: false } &&
+ !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter));
+
+ var controllerCount = _typeFinder.Types.Count(t =>
+ typeof(ControllerBase).IsAssignableFrom(t) &&
+ !t.IsAbstract &&
+ !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter));
+
+
+ context.Current[ActivityPropertyNames.AppServiceCount] = appServiceCount;
+ context.Current[ActivityPropertyNames.ControllerCount] = controllerCount;
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..f9e35084b9
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Properties/AssemblyInfo.cs
@@ -0,0 +1,20 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Volo.Abp.Authorization.Abstractions")]
+[assembly: AssemblyTrademark("")]
+[assembly: InternalsVisibleTo("Volo.Abp.Authorization")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("83632bec-2f60-4c2b-b964-30e8b37fa9d8")]
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
index ab49e4aad4..0f5d713e2b 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
@@ -108,6 +108,8 @@ public class PermissionDefinition :
Parent = this
};
+ child[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName];
+
_children.Add(child);
return child;
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
index 85d771a6ab..394cdb9d82 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
@@ -11,6 +11,13 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
public Dictionary Groups { get; }
+ internal IPermissionDefinitionProvider? CurrentProvider { get; set; }
+
+ public static class KnownPropertyNames
+ {
+ public const string CurrentProviderName = "_CurrentProviderName";
+ }
+
public PermissionDefinitionContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
@@ -28,7 +35,16 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
throw new AbpException($"There is already an existing permission group with name: {name}");
}
- return Groups[name] = new PermissionGroupDefinition(name, displayName);
+ var group = new PermissionGroupDefinition(name, displayName);
+
+ if (CurrentProvider != null)
+ {
+ group[KnownPropertyNames.CurrentProviderName] = CurrentProvider.GetType().FullName;
+ }
+
+ Groups[name] = group;
+
+ return group;
}
[NotNull]
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs
index bbc6e96cdf..0ad2dca099 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs
@@ -61,6 +61,8 @@ public class PermissionGroupDefinition : ICanAddChildPermission
isEnabled
);
+ permission[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName];
+
_permissions.Add(permission);
return permission;
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
index 2d0a9d668c..4e6ff0d11c 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
@@ -84,18 +84,23 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
foreach (var provider in providers)
{
+ context.CurrentProvider = provider;
provider.PreDefine(context);
}
foreach (var provider in providers)
{
+ context.CurrentProvider = provider;
provider.Define(context);
}
foreach (var provider in providers)
{
+ context.CurrentProvider = provider;
provider.PostDefine(context);
}
+
+ context.CurrentProvider = null;
return context.Groups;
}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs
new file mode 100644
index 0000000000..4fab06ba8f
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/TelemetryPermissionInfoEnricher.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Authorization.Permissions;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+public sealed class TelemetryPermissionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly IPermissionDefinitionManager _permissionDefinitionManager;
+
+ public TelemetryPermissionInfoEnricher(IPermissionDefinitionManager permissionDefinitionManager,
+ IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _permissionDefinitionManager = permissionDefinitionManager;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(context.ProjectId.HasValue);
+ }
+
+ protected async override Task ExecuteAsync(ActivityContext context)
+ {
+ var permissions = await _permissionDefinitionManager.GetPermissionsAsync();
+
+ var userDefinedPermissionsCount = permissions.Count(IsUserDefinedPermission);
+
+ context.Current[ActivityPropertyNames.PermissionCount] = userDefinedPermissionsCount;
+ }
+
+ private static bool IsUserDefinedPermission(PermissionDefinition permission)
+ {
+ return permission.Properties.TryGetValue(PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName, out var providerName) &&
+ providerName is string &&
+ !providerName.ToString()!.StartsWith(TelemetryConsts.VoloNameSpaceFilter);
+ }
+
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs
index 212e09c631..5f546717cb 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs
@@ -48,11 +48,6 @@ public class InstallLibsService : IInstallLibsService, ITransientDependency
return;
}
- if (!NpmHelper.IsYarnAvailable())
- {
- Logger.LogWarning("YARN is not installed, which may cause package inconsistency. ABP uses 'npx yarn ' behind the scenes to prevent possible inconsistencies.");
- }
-
Logger.LogInformation($"Found {projectPaths.Count} projects.");
foreach (var projectPath in projectPaths)
{
diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj
index caa6b6be94..5afcca9fbe 100644
--- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj
+++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj
@@ -22,6 +22,7 @@
+
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs
index ec20467ac9..6811e2a9a1 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs
@@ -4,13 +4,17 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using JetBrains.Annotations;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
using Volo.Abp.Logging;
using Volo.Abp.Modularity;
+using Volo.Abp.Threading;
namespace Volo.Abp;
@@ -149,6 +153,56 @@ public abstract class AbpApplicationBase : IAbpApplication
options.PlugInSources
);
}
+ protected void SetupTelemetryTracking()
+ {
+ if (!ShouldSendTelemetryData())
+ {
+ return;
+ }
+
+ AsyncHelper.RunSync(InitializeTelemetryTracking);
+ }
+
+ protected async Task SetupTelemetryTrackingAsync()
+ {
+ if (!ShouldSendTelemetryData())
+ {
+ return;
+ }
+
+ await InitializeTelemetryTracking();
+ }
+
+ private async Task InitializeTelemetryTracking()
+ {
+ try
+ {
+ using var scope = ServiceProvider.CreateScope();
+ var telemetryService = scope.ServiceProvider.GetRequiredService();
+ await telemetryService.AddActivityAsync(ActivityNameConsts.ApplicationRun);
+ }
+ catch (Exception ex)
+ {
+ try
+ {
+ using var scope = ServiceProvider.CreateScope();
+ var logger = scope.ServiceProvider.GetRequiredService>();
+ logger.LogException(ex, LogLevel.Trace);
+ }
+ catch
+ {
+ /* ignored */
+ }
+ }
+ }
+
+ private bool ShouldSendTelemetryData()
+ {
+ using var scope = ServiceProvider.CreateScope();
+ var abpHostEnvironment = scope.ServiceProvider.GetRequiredService();
+ var configuration = scope.ServiceProvider.GetRequiredService();
+ return abpHostEnvironment.IsDevelopment() && configuration.GetValue("Abp:Telemetry:IsEnabled") == true;
+ }
//TODO: We can extract a new class for this
public virtual async Task ConfigureServicesAsync()
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs
index d80e46c5d7..faf4f7fa1a 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs
@@ -44,6 +44,8 @@ internal class AbpApplicationWithExternalServiceProvider : AbpApplicationBase, I
SetServiceProvider(serviceProvider);
await InitializeModulesAsync();
+
+ await SetupTelemetryTrackingAsync();
}
public void Initialize([NotNull] IServiceProvider serviceProvider)
@@ -53,6 +55,8 @@ internal class AbpApplicationWithExternalServiceProvider : AbpApplicationBase, I
SetServiceProvider(serviceProvider);
InitializeModules();
+
+ SetupTelemetryTracking();
}
public override void Dispose()
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs
index c5e5eccc83..b652999a7a 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs
@@ -50,12 +50,15 @@ internal class AbpApplicationWithInternalServiceProvider : AbpApplicationBase, I
{
CreateServiceProvider();
await InitializeModulesAsync();
+ await SetupTelemetryTrackingAsync();
+
}
public void Initialize()
{
CreateServiceProvider();
InitializeModules();
+ SetupTelemetryTracking();
}
public override void Dispose()
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs
new file mode 100644
index 0000000000..8a03a66e65
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityContext.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+namespace Volo.Abp.Internal.Telemetry.Activity;
+
+public class ActivityContext
+{
+ public ActivityEvent Current { get; }
+ public Dictionary ExtraProperties { get; } = new();
+ public bool IsTerminated { get; private set; }
+
+ public Guid? ProjectId => Current.Get(ActivityPropertyNames.ProjectId);
+
+ public Guid? SolutionId => Current.Get(ActivityPropertyNames.SolutionId);
+
+ public SessionType? SessionType => Current.Get(ActivityPropertyNames.SessionType);
+
+ public string? DeviceId => Current.Get(ActivityPropertyNames.DeviceId);
+
+ public string? SolutionPath => ExtraProperties.TryGetValue(ActivityPropertyNames.SolutionPath, out var solutionPath)
+ ? solutionPath?.ToString()
+ : null;
+
+ private ActivityContext(ActivityEvent current)
+ {
+ Current = current;
+ }
+
+ public static ActivityContext Create(string activityName, string? details = null,
+ Action>? additionalProperties = null)
+ {
+ var activity = new ActivityEvent(activityName, details);
+
+ if (additionalProperties is not null)
+ {
+ var additionalPropertiesDict = new Dictionary();
+ activity[ActivityPropertyNames.AdditionalProperties] = additionalPropertiesDict;
+ additionalProperties.Invoke(additionalPropertiesDict);
+ }
+
+ return new ActivityContext(activity);
+ }
+
+ public void Terminate()
+ {
+ IsTerminated = true;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs
new file mode 100644
index 0000000000..22ad4c0da3
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/ActivityEvent.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Internal.Telemetry.Activity;
+
+public class ActivityEvent : Dictionary
+{
+ public ActivityEvent()
+ {
+ this[ActivityPropertyNames.Id] = Guid.NewGuid();
+ this[ActivityPropertyNames.Time] = DateTimeOffset.UtcNow;
+ }
+
+ public ActivityEvent(string activityName, string? details = null) : this()
+ {
+ Check.NotNullOrWhiteSpace(activityName, nameof(activityName));
+
+ this[ActivityPropertyNames.ActivityName] = activityName;
+ this[ActivityPropertyNames.ActivityDetails] = details;
+ }
+
+ public bool HasSolutionInfo()
+ {
+ return this.ContainsKey(ActivityPropertyNames.HasSolutionInfo);
+ }
+
+ public bool HasDeviceInfo()
+ {
+ return this.ContainsKey(ActivityPropertyNames.HasDeviceInfo);
+ }
+
+ public bool HasProjectInfo()
+ {
+ return this.ContainsKey(ActivityPropertyNames.HasProjectInfo);
+ }
+
+ public T Get(string key)
+ {
+ return TryConvert(key, out var value) ? value : default!;
+ }
+
+ public bool TryGetValue(string key, out T value)
+ {
+ return TryConvert(key, out value);
+ }
+
+ private bool TryConvert(string key, out T result)
+ {
+ result = default!;
+ if (!this.TryGetValue(key, out var value) || value is null)
+ {
+ return false;
+ }
+
+ try
+ {
+ if (value is T tValue)
+ {
+ result = tValue;
+ return true;
+ }
+
+ if (value is JsonElement jsonElement)
+ {
+ value = ExtractFromJsonElement(jsonElement);
+ if (value is null)
+ {
+ return false;
+ }
+ }
+
+ var targetType = typeof(T);
+ var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
+ if (underlyingType.IsEnum)
+ {
+ if (value is string str)
+ {
+ result = (T)Enum.Parse(underlyingType, str, ignoreCase: true);
+ }
+ else if (value is int intValue)
+ {
+ result = (T)Enum.ToObject(underlyingType, intValue);
+ }
+
+ return true;
+ }
+
+
+ if (underlyingType == typeof(Dictionary[]))
+ {
+ result = (T)value;
+ return true;
+ }
+
+ if (underlyingType == typeof(Guid))
+ {
+ result = (T)(object)Guid.Parse(value.ToString()!);
+ return true;
+ }
+
+ if (underlyingType == typeof(DateTimeOffset))
+ {
+ result = (T)(object)DateTimeOffset.Parse(value.ToString()!);
+ return true;
+ }
+
+ // Nullable types
+ result = (T)Convert.ChangeType(value, underlyingType);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static object? ExtractFromJsonElement(JsonElement element)
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.String => element.GetString(),
+ JsonValueKind.Number => element.GetInt32(),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => null,
+ JsonValueKind.Array => element.EnumerateArray()
+ .Select(item =>
+ {
+ if (item.ValueKind == JsonValueKind.Object)
+ {
+ return item.EnumerateObject()
+ .ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value));
+ }
+
+ return new Dictionary { { "value", ExtractFromJsonElement(item) } };
+ })
+ .ToArray(),
+
+ JsonValueKind.Object => element.EnumerateObject()
+ .ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value)),
+ _ => element.ToString()
+ };
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs
new file mode 100644
index 0000000000..5547445d54
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/IHasParentTelemetryActivityEventEnricher.cs
@@ -0,0 +1,7 @@
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Contracts;
+
+public interface IHasParentTelemetryActivityEventEnricher where TParent: TelemetryActivityEventEnricher
+{
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs
new file mode 100644
index 0000000000..0542b9d676
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventBuilder.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Contracts;
+
+public interface ITelemetryActivityEventBuilder
+{
+ Task BuildAsync(ActivityContext context);
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs
new file mode 100644
index 0000000000..b83741cd88
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityEventEnricher.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Contracts;
+
+public interface ITelemetryActivityEventEnricher
+{
+ int ExecutionOrder { get; }
+
+ Task EnrichAsync(ActivityContext context);
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs
new file mode 100644
index 0000000000..870876ffbd
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Contracts/ITelemetryActivityStorage.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Contracts;
+
+public interface ITelemetryActivityStorage
+{
+ Guid InitializeOrGetSession();
+ void DeleteActivities(ActivityEvent[] activities);
+ void SaveActivity(ActivityEvent activityEvent);
+ List GetActivities();
+ bool ShouldAddDeviceInfo();
+ bool ShouldAddSolutionInformation(Guid solutionId);
+ bool ShouldAddProjectInfo(Guid projectId);
+ bool ShouldSendActivities();
+ void MarkActivitiesAsFailed(ActivityEvent[] activities);
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs
new file mode 100644
index 0000000000..201ac8c3b6
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventBuilder.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+public class TelemetryActivityEventBuilder : ITelemetryActivityEventBuilder, ISingletonDependency
+{
+ private readonly List _activityEnrichers;
+
+ public TelemetryActivityEventBuilder(IEnumerable activityDataEnrichers)
+ {
+ _activityEnrichers = activityDataEnrichers
+ .Where(FilterEnricher)
+ .OrderByDescending(x => x.ExecutionOrder)
+ .ToList();
+ }
+ public virtual async Task BuildAsync(ActivityContext context)
+ {
+ foreach (var enricher in _activityEnrichers)
+ {
+ try
+ {
+ await enricher.EnrichAsync(context);
+ }
+ catch
+ {
+ //ignored
+ }
+
+ if (context.IsTerminated)
+ {
+ return null;
+ }
+ }
+
+ return context.Current;
+ }
+
+ private static bool FilterEnricher(ITelemetryActivityEventEnricher enricher)
+ {
+ return ProxyHelper.GetUnProxiedType(enricher).Assembly.FullName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter) &&
+ enricher is not IHasParentTelemetryActivityEventEnricher;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs
new file mode 100644
index 0000000000..d25e9c58f8
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryActivityEventEnricher.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+public abstract class TelemetryActivityEventEnricher : ITelemetryActivityEventEnricher, IScopedDependency
+{
+ public virtual int ExecutionOrder { get; set; } = 0;
+ protected bool IgnoreChildren { get; set; }
+ protected virtual Type? ReplaceParentType { get; set; }
+
+ private readonly IServiceProvider _serviceProvider;
+
+ protected TelemetryActivityEventEnricher(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public async Task EnrichAsync(ActivityContext context)
+ {
+ if (!await CanExecuteAsync(context))
+ {
+ return;
+ }
+
+ await ExecuteAsync(context);
+ await ExecuteChildrenAsync(context);
+ }
+
+ protected virtual Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(true);
+ }
+
+ protected abstract Task ExecuteAsync(ActivityContext context);
+
+ protected virtual async Task ExecuteChildrenAsync(ActivityContext context)
+ {
+ if (IgnoreChildren)
+ {
+ return;
+ }
+
+ using var scope = _serviceProvider.CreateScope();
+
+ foreach (var child in GetChildren(scope.ServiceProvider))
+ {
+ await child.EnrichAsync(context);
+ }
+ }
+
+ private ITelemetryActivityEventEnricher[] GetChildren(IServiceProvider serviceProvider)
+ {
+ var targetType = ReplaceParentType ?? ProxyHelper.GetUnProxiedType(this);
+ var genericInterfaceType = typeof(IHasParentTelemetryActivityEventEnricher<>).MakeGenericType(targetType);
+ var enumerableType = typeof(IEnumerable<>).MakeGenericType(genericInterfaceType);
+
+ var childServices = (IEnumerable)serviceProvider.GetRequiredService(enumerableType);
+
+ return childServices
+ .Cast()
+ .ToArray();
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs
new file mode 100644
index 0000000000..6616c62bda
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryApplicationInfoEnricher.cs
@@ -0,0 +1,104 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.Helpers;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+public sealed class TelemetryApplicationInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly ITelemetryActivityStorage _telemetryActivityStorage;
+
+ public TelemetryApplicationInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _telemetryActivityStorage = telemetryActivityStorage;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime);
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ try
+ {
+ var entryAssembly = Assembly.GetEntryAssembly();
+ if (entryAssembly is null)
+ {
+ context.Terminate();
+ return Task.CompletedTask;
+ }
+
+ var projectMetaData = AbpProjectMetadataReader.ReadProjectMetadata(entryAssembly);
+ if (projectMetaData?.ProjectId == null || projectMetaData.AbpSlnPath.IsNullOrEmpty())
+ {
+ context.Terminate();
+ return Task.CompletedTask;
+ }
+
+ if (!_telemetryActivityStorage.ShouldAddProjectInfo(projectMetaData.ProjectId.Value))
+ {
+ IgnoreChildren = true;
+ return Task.CompletedTask;
+ }
+
+ var solutionId = ReadSolutionIdFromSolutionPath(projectMetaData.AbpSlnPath);
+
+ if (!solutionId.HasValue)
+ {
+ IgnoreChildren = true;
+ context.Terminate();
+ return Task.CompletedTask;
+ }
+
+ context.ExtraProperties[ActivityPropertyNames.SolutionPath] = projectMetaData.AbpSlnPath;
+ context.Current[ActivityPropertyNames.ProjectType] = projectMetaData.Role ?? string.Empty;
+ context.Current[ActivityPropertyNames.ProjectId] = projectMetaData.ProjectId.Value;
+ context.Current[ActivityPropertyNames.SolutionId] = solutionId;
+ context.Current[ActivityPropertyNames.HasProjectInfo] = true;
+ }
+ catch
+ {
+ //ignored
+ }
+
+ return Task.CompletedTask;
+ }
+
+
+ private static Guid? ReadSolutionIdFromSolutionPath(string solutionPath)
+ {
+ try
+ {
+ if (solutionPath.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ using var fs = new FileStream(solutionPath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var doc = JsonDocument.Parse(fs, new JsonDocumentOptions
+ {
+ AllowTrailingCommas = true
+ });
+ if (doc.RootElement.TryGetProperty("id", out var property) && property.TryGetGuid(out var solutionId))
+ {
+ return solutionId;
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs
new file mode 100644
index 0000000000..5081519b40
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryDeviceInfoEnricher.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher))]
+internal sealed class TelemetryDeviceInfoEnricher : TelemetryActivityEventEnricher
+{
+ private readonly ITelemetryActivityStorage _telemetryActivityStorage;
+ private readonly ISoftwareInfoProvider _softwareInfoProvider;
+
+ public TelemetryDeviceInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage,
+ ISoftwareInfoProvider softwareInfoProvider, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _telemetryActivityStorage = telemetryActivityStorage;
+ _softwareInfoProvider = softwareInfoProvider;
+ }
+ protected async override Task ExecuteAsync(ActivityContext context)
+ {
+ try
+ {
+ var deviceId = DeviceManager.GetUniquePhysicalKey(true);
+ context.Current[ActivityPropertyNames.DeviceId] = deviceId;
+
+ if (!_telemetryActivityStorage.ShouldAddDeviceInfo())
+ {
+ return;
+ }
+
+ var softwareList = await _softwareInfoProvider.GetSoftwareInfoAsync();
+
+ context.Current[ActivityPropertyNames.InstalledSoftwares] = softwareList;
+ context.Current[ActivityPropertyNames.DeviceLanguage] = CultureInfo.CurrentUICulture.Name;
+ context.Current[ActivityPropertyNames.OperatingSystem] = GetOperatingSystem();
+ context.Current[ActivityPropertyNames.CountryIsoCode] = GetCountry();
+ context.Current[ActivityPropertyNames.HasDeviceInfo] = true;
+ context.Current[ActivityPropertyNames.OperatingSystemArchitecture] = RuntimeInformation.OSArchitecture.ToString();
+ }
+ catch
+ {
+ //ignored
+ }
+ }
+
+ private static OperationSystem GetOperatingSystem()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return OperationSystem.Windows;
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return OperationSystem.Linux;
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return OperationSystem.MacOS;
+ }
+
+ return OperationSystem.Unknown;
+ }
+
+
+
+ private static string GetCountry()
+ {
+ var region = new RegionInfo(CultureInfo.InstalledUICulture.Name);
+ return region.TwoLetterISORegionName;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs
new file mode 100644
index 0000000000..d35973b79d
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetryModuleInfoEnricher.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Modularity;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+internal sealed class TelemetryModuleInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly IModuleContainer _moduleContainer;
+ private readonly IAssemblyFinder _assemblyFinder;
+
+ public TelemetryModuleInfoEnricher(IModuleContainer moduleContainer, IAssemblyFinder assemblyFinder,
+ IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _moduleContainer = moduleContainer;
+ _assemblyFinder = assemblyFinder;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime);
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ context.Current[ActivityPropertyNames.ModuleCount] = _moduleContainer.Modules.Count;
+ context.Current[ActivityPropertyNames.ProjectCount] = _assemblyFinder.Assemblies.Count(x =>
+ !x.FullName.IsNullOrEmpty() &&
+ !x.FullName.StartsWith(TelemetryConsts.VoloNameSpaceFilter));
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs
new file mode 100644
index 0000000000..a015d40733
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySessionInfoEnricher.cs
@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+[ExposeServices(typeof(ITelemetryActivityEventEnricher))]
+public class TelemetrySessionInfoEnricher : TelemetryActivityEventEnricher
+{
+ public override int ExecutionOrder { get; set; } = 10;
+
+ public TelemetrySessionInfoEnricher(IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ context.Current[ActivityPropertyNames.SessionType] = SessionType.ApplicationRuntime;
+ context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid().ToString();
+ context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage);
+
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs
new file mode 100644
index 0000000000..785ca0dfa1
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Providers;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+internal sealed class TelemetrySolutionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly ITelemetryActivityStorage _telemetryActivityStorage;
+
+ public TelemetrySolutionInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _telemetryActivityStorage = telemetryActivityStorage;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ if (context.SolutionId.HasValue && !context.SolutionPath.IsNullOrEmpty())
+ {
+ return Task.FromResult(_telemetryActivityStorage.ShouldAddSolutionInformation(context.SolutionId.Value));
+ }
+
+ return Task.FromResult(false);
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ try
+ {
+ var jsonContent = File.ReadAllText(context.SolutionPath!);
+ using var doc = JsonDocument.Parse(jsonContent, new JsonDocumentOptions
+ {
+ AllowTrailingCommas = true
+ });
+
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("creatingStudioConfiguration", out var creatingStudioConfiguration))
+ {
+ AddSolutionCreationConfiguration(context, creatingStudioConfiguration);
+ }
+
+ if (root.TryGetProperty("modules", out var modulesElement))
+ {
+ AddModuleInfo(context, modulesElement);
+ }
+
+ context.Current[ActivityPropertyNames.HasSolutionInfo] = true;
+ }
+ catch
+ {
+ //ignored
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static void AddSolutionCreationConfiguration(ActivityContext context, JsonElement config)
+ {
+ context.Current[ActivityPropertyNames.Template] = TelemetryJsonExtensions.GetStringOrNull(config, "template");
+ context.Current[ActivityPropertyNames.CreatedAbpStudioVersion] = TelemetryJsonExtensions.GetStringOrNull(config, "createdAbpStudioVersion");
+ context.Current[ActivityPropertyNames.MultiTenancy] = TelemetryJsonExtensions.GetBooleanOrNull(config, "multiTenancy");
+ context.Current[ActivityPropertyNames.UiFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "uiFramework");
+ context.Current[ActivityPropertyNames.DatabaseProvider] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseProvider");
+ context.Current[ActivityPropertyNames.Theme] = TelemetryJsonExtensions.GetStringOrNull(config, "theme");
+ context.Current[ActivityPropertyNames.ThemeStyle] = TelemetryJsonExtensions.GetStringOrNull(config, "themeStyle");
+ context.Current[ActivityPropertyNames.HasPublicWebsite] = TelemetryJsonExtensions.GetBooleanOrNull(config, "publicWebsite");
+ context.Current[ActivityPropertyNames.IsTiered] = TelemetryJsonExtensions.GetBooleanOrNull(config, "tiered");
+ context.Current[ActivityPropertyNames.SocialLogins] = TelemetryJsonExtensions.GetBooleanOrNull(config, "socialLogin");
+ context.Current[ActivityPropertyNames.DatabaseManagementSystem] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseManagementSystem");
+ context.Current[ActivityPropertyNames.IsSeparateTenantSchema] = TelemetryJsonExtensions.GetBooleanOrNull(config, "separateTenantSchema");
+ context.Current[ActivityPropertyNames.MobileFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "mobileFramework");
+ context.Current[ActivityPropertyNames.IncludeTests] = TelemetryJsonExtensions.GetBooleanOrNull(config, "includeTests");
+ context.Current[ActivityPropertyNames.DynamicLocalization] = TelemetryJsonExtensions.GetBooleanOrNull(config, "dynamicLocalization");
+ context.Current[ActivityPropertyNames.KubernetesConfiguration] = TelemetryJsonExtensions.GetBooleanOrNull(config, "kubernetesConfiguration");
+ context.Current[ActivityPropertyNames.GrafanaDashboard] = TelemetryJsonExtensions.GetBooleanOrNull(config, "grafanaDashboard");
+ }
+
+ private static void AddModuleInfo(ActivityContext context, JsonElement modulesElement)
+ {
+ var modules = new List>();
+
+ foreach (var module in modulesElement.EnumerateObject())
+ {
+ var modulePath = GetModuleFilePath(context.SolutionPath!, module);
+ if (modulePath.IsNullOrEmpty())
+ {
+ continue;
+ }
+
+ var moduleJsonFileContent = File.ReadAllText(modulePath);
+ using var moduleDoc = JsonDocument.Parse(moduleJsonFileContent);
+
+ if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports))
+ {
+ continue;
+ }
+
+ foreach (var import in imports.EnumerateObject())
+ {
+ modules.Add(new Dictionary
+ {
+ { ActivityPropertyNames.ModuleName, import.Name },
+ { ActivityPropertyNames.ModuleVersion, TelemetryJsonExtensions.GetStringOrNull(import.Value, "version") },
+ { ActivityPropertyNames.ModuleInstallationTime, TelemetryJsonExtensions.GetDateTimeOffsetOrNull(import.Value, "creationTime") }
+ });
+ }
+ }
+
+ context.Current[ActivityPropertyNames.InstalledModules] = modules;
+ }
+
+ private static string? GetModuleFilePath(string solutionPath, JsonProperty module)
+ {
+ var path = TelemetryJsonExtensions.GetStringOrNull(module.Value, "path");
+ if (path.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var fullPath = Path.Combine(Path.GetDirectoryName(solutionPath)!, path);
+ return File.Exists(fullPath) ? fullPath : null;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs
new file mode 100644
index 0000000000..d12565d5d2
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/FailedActivityInfo.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Storage;
+
+internal class FailedActivityInfo
+{
+ public DateTimeOffset FirstFailTime { get; set; }
+ public DateTimeOffset LastFailTime { get; set; }
+ public int RetryCount { get; set; }
+
+ public bool IsExpired()
+ {
+ var now = DateTimeOffset.UtcNow;
+
+ return RetryCount >= TelemetryPeriod.MaxActivityRetryCount ||
+ now - FirstFailTime > TelemetryPeriod.MaxFailedActivityAge;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs
new file mode 100644
index 0000000000..70455b2d45
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorage.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Helpers;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Storage;
+
+public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDependency
+{
+ private TelemetryActivityStorageState State { get; }
+ private readonly static JsonSerializerOptions JsonSerializerOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ public TelemetryActivityStorage()
+ {
+ CreateDirectoryIfNotExist();
+
+ State = LoadState();
+ }
+
+ public void SaveActivity(ActivityEvent activityEvent)
+ {
+ State.Activities.Add(activityEvent);
+
+ var activityName = activityEvent.Get(ActivityPropertyNames.ActivityName);
+
+ if (activityName == ActivityNameConsts.AbpStudioClose)
+ {
+ State.SessionId = null;
+ }
+
+ if (activityEvent.HasDeviceInfo())
+ {
+ State.LastDeviceInfoAddTime = DateTimeOffset.UtcNow;
+ }
+
+ if (activityEvent.HasSolutionInfo())
+ {
+ var solutionId = activityEvent.Get(ActivityPropertyNames.SolutionId);
+ State.Solutions[solutionId] = DateTimeOffset.UtcNow;
+ }
+
+ if (activityEvent.HasProjectInfo())
+ {
+ var projectId = activityEvent.Get(ActivityPropertyNames.ProjectId);
+ State.Projects[projectId] = DateTimeOffset.UtcNow;
+ }
+
+ SaveState();
+ }
+
+ public List GetActivities()
+ {
+ return State.Activities;
+ }
+
+ public Guid InitializeOrGetSession()
+ {
+ if (State.SessionId.HasValue)
+ {
+ return State.SessionId.Value;
+ }
+
+ State.SessionId = Guid.NewGuid();
+ SaveState();
+
+ return State.SessionId.Value;
+ }
+
+ public void DeleteActivities(ActivityEvent[] activities)
+ {
+ var activityIds = new HashSet(activities.Select(x => x.Get(ActivityPropertyNames.Id)));
+
+ State.Activities.RemoveAll(x => activityIds.Contains(x.Get(ActivityPropertyNames.Id)));
+
+ SaveState();
+ }
+
+ public void MarkActivitiesAsFailed(ActivityEvent[] activities)
+ {
+ var now = DateTimeOffset.UtcNow;
+
+ foreach (var activity in activities)
+ {
+ var activityId = activity.Get(ActivityPropertyNames.Id);
+
+ if (State.FailedActivities.TryGetValue(activityId, out var failedActivityInfo))
+ {
+ failedActivityInfo.RetryCount++;
+ failedActivityInfo.LastFailTime = now;
+
+ if (!failedActivityInfo.IsExpired())
+ {
+ continue;
+ }
+
+ State.Activities.RemoveAll(x=> x.Get(ActivityPropertyNames.Id) == activityId);
+ State.FailedActivities.Remove(activityId);
+ }
+ else
+ {
+ State.FailedActivities[activityId] = new FailedActivityInfo
+ {
+ FirstFailTime = now,
+ LastFailTime = now,
+ RetryCount = 1
+ };
+ }
+ }
+
+ SaveState();
+ }
+
+ public bool ShouldAddDeviceInfo()
+ {
+ return State.LastDeviceInfoAddTime is null ||
+ DateTimeOffset.UtcNow - State.LastDeviceInfoAddTime > TelemetryPeriod.InformationSendPeriod;
+ }
+
+ public bool ShouldAddSolutionInformation(Guid solutionId)
+ {
+ return !State.Solutions.TryGetValue(solutionId, out var lastSend) ||
+ DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod;
+ }
+
+ public bool ShouldAddProjectInfo(Guid projectId)
+ {
+ return !State.Projects.TryGetValue(projectId, out var lastSend) ||
+ DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod;
+ }
+
+ public bool ShouldSendActivities()
+ {
+ return State.ActivitySendTime is null ||
+ DateTimeOffset.UtcNow - State.ActivitySendTime > TelemetryPeriod.ActivitySendPeriod;
+ }
+
+ private void SaveState()
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(State, JsonSerializerOptions);
+ var encryptedJson = Cryptography.Encrypt(json);
+ File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8);
+ }
+ catch
+ {
+ // Ignored
+ }
+ }
+
+ private static TelemetryActivityStorageState LoadState()
+ {
+ try
+ {
+ if (!File.Exists(TelemetryPaths.ActivityStorage))
+ {
+ return new TelemetryActivityStorageState();
+ }
+
+ var fileContent = MutexExecutor.ReadFileSafely(TelemetryPaths.ActivityStorage);
+
+ if (fileContent.IsNullOrEmpty())
+ {
+ return new TelemetryActivityStorageState();
+ }
+
+ var json = Cryptography.Decrypt(fileContent);
+
+ var state = JsonSerializer.Deserialize(json, JsonSerializerOptions)!;
+ state.Activities = state.Activities.Where(x => x != null).ToList();
+ return state;
+ }
+ catch
+ {
+ return new TelemetryActivityStorageState();
+ }
+ }
+
+ private static void CreateDirectoryIfNotExist()
+ {
+ try
+ {
+ var storageDirectory = Path.GetDirectoryName(TelemetryPaths.ActivityStorage)!;
+
+ if (!Directory.Exists(storageDirectory))
+ {
+ Directory.CreateDirectory(storageDirectory);
+ }
+ }
+ catch
+ {
+ // Ignored
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs
new file mode 100644
index 0000000000..f0d1dfd4a5
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryActivityStorageState.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Storage;
+
+internal class TelemetryActivityStorageState
+{
+ public DateTimeOffset? ActivitySendTime { get; set; }
+ public DateTimeOffset? LastDeviceInfoAddTime { get; set; }
+ public Guid? SessionId { get; set; }
+ public List Activities { get; set; } = new();
+ public Dictionary Solutions { get; set; } = new();
+ public Dictionary Projects { get; set; } = new();
+ public Dictionary FailedActivities { get; set; } = new();
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs
new file mode 100644
index 0000000000..131180a3a4
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Storage/TelemetryPeriod.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Volo.Abp.Internal.Telemetry.Activity.Storage;
+
+static internal class TelemetryPeriod
+{
+ private const string TestModeEnvironmentVariable = "ABP_TELEMETRY_TEST_MODE";
+
+ static TelemetryPeriod()
+ {
+ var isTestMode = IsTestModeEnabled();
+
+ InformationSendPeriod = isTestMode
+ ? TimeSpan.FromSeconds(15)
+ : TimeSpan.FromDays(7);
+
+ ActivitySendPeriod = isTestMode
+ ? TimeSpan.FromSeconds(5)
+ : TimeSpan.FromDays(1);
+ }
+
+ public static TimeSpan ActivitySendPeriod { get; }
+ public static TimeSpan InformationSendPeriod { get; }
+
+ public static int MaxActivityRetryCount { get; set; } = 3;
+ public static TimeSpan MaxFailedActivityAge { get; set; } = TimeSpan.FromDays(30);
+
+ private static bool IsTestModeEnabled()
+ {
+ var testModeVariable =
+ Environment.GetEnvironmentVariable(TestModeEnvironmentVariable, EnvironmentVariableTarget.User);
+ return bool.TryParse(testModeVariable, out var isTestMode) && isTestMode;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs
new file mode 100644
index 0000000000..6f8b5c2a10
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/TelemetryJsonExtensions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Text.Json;
+
+namespace Volo.Abp.Internal.Telemetry.Activity;
+
+static internal class TelemetryJsonExtensions
+{
+ static internal string? GetStringOrNull(JsonElement element, string propertyName)
+ {
+ return element.TryGetProperty(propertyName, out var property)
+ ? property.GetString() ?? null
+ : null;
+ }
+
+ static internal bool? GetBooleanOrNull(JsonElement element, string propertyName)
+ {
+ if (element.TryGetProperty(propertyName, out var property) && bool.TryParse(property.GetString(), out var boolValue))
+ {
+ return boolValue;
+ }
+
+ return null;
+ }
+
+ static internal DateTimeOffset? GetDateTimeOffsetOrNull(JsonElement element, string propertyName)
+ {
+ if (element.TryGetProperty(propertyName, out var date) && DateTimeOffset.TryParse(date.GetString(), out var dateTimeValue))
+ {
+ return dateTimeValue;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs
new file mode 100644
index 0000000000..61f2907e8f
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/AbpPlatformUrls.cs
@@ -0,0 +1,12 @@
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+public static class AbpPlatformUrls
+{
+#if DEBUG
+ public const string AbpTelemetryApiUrl = "https://localhost:44393/";
+#else
+ public const string AbpTelemetryApiUrl = "https://telemetry.abp.io/";
+#endif
+
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs
new file mode 100644
index 0000000000..64b22ef78f
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs
@@ -0,0 +1,76 @@
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+public static class ActivityNameConsts
+{
+ public const string AbpStudioOpen = "AbpStudio.Open";
+ public const string AbpStudioOpenFirstTimeForDevice = "AbpStudio.Open.FirstTimeForDevice";
+ public const string AbpStudioOpenFirstTimeForUser = "AbpStudio.Open.FirstTimeForUser";
+ public const string AbpStudioClose = "AbpStudio.Close";
+ public const string AbpStudioCloseWithoutLogin = "AbpStudio.Close.WithoutLogin";
+ public const string AbpStudioLogin = "AbpStudio.Login";
+ public const string AbpStudioCampaignClick = "AbpStudio.Campaing.Click";
+ public const string AbpStudioCommunityPostClick = "AbpStudio.CommunityPost.Click";
+ public const string AbpStudioSolutionInitExisting = "AbpStudio.Solution.InitExisting";
+ public const string AbpStudioSolutionNew = "AbpStudio.Solution.New";
+ public const string AbpStudioSolutionNewMicroservice = "AbpStudio.Solution.New.Microservice";
+ public const string AbpStudioSolutionAddModule = "AbpStudio.Solution.Add.Module";
+ public const string AbpStudioSolutionAddModuleEmpty = "AbpStudio.Solution.Add.Module.Empty";
+ public const string AbpStudioSolutionAddModuleDdd = "AbpStudio.Solution.Add.Module.Ddd";
+ public const string AbpStudioSolutionAddModuleStandard = "AbpStudio.Solution.Add.Module.Standard";
+ public const string AbpStudioSolutionAddMicroservice = "AbpStudio.Solution.Add.Microservice";
+ public const string AbpStudioSolutionAddGateway = "AbpStudio.Solution.Add.Gateway";
+ public const string AbpStudioSolutionAddWeb = "AbpStudio.Solution.Add.Web";
+ public const string AbpStudioSolutionAddPackage = "AbpStudio.Solution.Add.Package";
+ public const string AbpStudioSolutionAddPackageHttpApiLayer = "AbpStudio.Solution.Add.Package.HttpApiLayer";
+ public const string AbpStudioAbpCliInstallLibs = "AbpStudio.AbpCli.InstallLibs";
+ public const string AbpStudioAbpCliUpgradeAbp = "AbpStudio.AbpCli.UpgradeAbp";
+ public const string AbpStudioAbpCliSwitchToStable = "AbpStudio.AbpCli.SwitchToStable";
+ public const string AbpStudioAbpCliSwitchToPreview = "AbpStudio.AbpCli.SwitchToPreview";
+ public const string AbpStudioAbpCliSwitchToNightly = "AbpStudio.AbpCli.SwitchToNightly";
+ public const string AbpStudioAbpCliClean = "AbpStudio.AbpCli.Clean";
+ public const string AbpStudioDotnetCliBuild = "AbpStudio.DotnetCli.Build";
+ public const string AbpStudioDotnetCliGraphBuild = "AbpStudio.DotnetCli.GraphBuild";
+ public const string AbpStudioDotnetCliClean = "AbpStudio.DotnetCli.Clean";
+ public const string AbpStudioDotnetCliRestore = "AbpStudio.DotnetCli.Restore";
+ public const string AbpStudioSolutionRunnerRunApp = "AbpStudio.SolutionRunner.RunApp";
+ public const string AbpStudioSolutionRunnerAddCsharpApp = "AbpStudio.SolutionRunner.Add.CsharpApp";
+ public const string AbpStudioSolutionRunnerAddCliApp = "AbpStudio.SolutionRunner.Add.CliApp";
+ public const string AbpStudioSolutionRunnerAddProfile = "AbpStudio.SolutionRunner.Add.Profile";
+ public const string AbpStudioSolutionRunnerAppManageMetadata = "AbpStudio.SolutionRunner.App.Manage.Metadata";
+ public const string AbpStudioSolutionRunnerAppManageSecrets = "AbpStudio.SolutionRunner.App.Manage.Secrets";
+ public const string AbpStudioSolutionOpen = "AbpStudio.Solution.Open";
+ public const string AbpStudioMonitoringBrowse = "AbpStudio.Monitoring.Browse";
+ public const string AbpStudioMonitoringHttpRequests = "AbpStudio.Monitoring.HttpRequests";
+ public const string AbpStudioMonitoringHttpRequestsDetail = "AbpStudio.Monitoring.HttpRequests.Detail";
+ public const string AbpStudioMonitoringEvents = "AbpStudio.Monitoring.Events";
+ public const string AbpStudioMonitoringEventsDetail = "AbpStudio.Monitoring.Events.Detail";
+ public const string AbpStudioMonitoringExceptions = "AbpStudio.Monitoring.Exceptions";
+ public const string AbpStudioMonitoringExceptionsDetail = "AbpStudio.Monitoring.Exceptions.Detail";
+ public const string AbpStudioMonitoringLogs = "AbpStudio.Monitoring.Logs";
+ public const string AbpStudioKubernetesAddProfile = "AbpStudio.Kubernetes.Add.Profile";
+ public const string AbpStudioKubernetesConnect = "AbpStudio.Kubernetes.Connect";
+ public const string AbpStudioKubernetesIntercept = "AbpStudio.Kubernetes.Intercept";
+ public const string AbpStudioKubernetesHelmCommandsInstall = "AbpStudio.Kubernetes.Helm.Commands.Install";
+ public const string AbpStudioKubernetesHelmCommandsBuildImages = "AbpStudio.Kubernetes.Helm.Commands.BuildImages";
+ public const string AbpStudioKubernetesHelmChartsRefreshSubCharts = "AbpStudio.Kubernetes.Helm.Charts.RefreshSubCharts";
+ public const string AbpStudioKubernetesHelmChartsManageMetadata = "AbpStudio.Kubernetes.Helm.Charts.Manage.Metadata";
+ public const string AbpStudioLogsShow = "AbpStudio.Logs.Show";
+ public const string AbpStudioSuiteOpen = "AbpStudio.Suite.Open";
+ public const string AbpStudioGlobalSecretsManage = "AbpStudio.GlobalSecrets.Manage";
+ public const string AbpStudioGlobalMetadataManage = "AbpStudio.GlobalMetadata.Manage";
+ public const string AbpCliCommandsNewSolution = "AbpCli.Comands.NewSolution";
+ public const string AbpCliCommandsNewModule = "AbpCli.Comands.NewModule";
+ public const string AbpCliCommandsNewPackage = "AbpCli.Comands.NewPackage";
+ public const string AbpCliCommandsUpdate = "AbpCli.Comands.Update";
+ public const string AbpCliCommandsClean = "AbpCli.Comands.Clean";
+ public const string AbpCliCommandsAddPackage = "AbpCli.Comands.AddPackage";
+ public const string AbpCliCommandsAddPackageRef = "AbpCli.Comands.AddPackageRef";
+ public const string AbpCliCommandsInstallModule = "AbpCli.Comands.InstallModule";
+ public const string AbpCliCommandsInstallLocalModule = "AbpCli.Comands.InstallLocalModule";
+ public const string AbpCliCommandsListModules = "AbpCli.Comands.ListModules";
+ public const string AbpCliRun = "AbpCli.Run";
+ public const string AbpCliExit = "AbpCli.Exit";
+ public const string ApplicationRun = "Application.Run";
+ public const string AbpStudioBrowserOpen = "AbpStudio.Browser.Open";
+ public const string Error = "Error";
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs
new file mode 100644
index 0000000000..68f7e95f16
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityPropertyNames.cs
@@ -0,0 +1,77 @@
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+public static class ActivityPropertyNames
+{
+ public const string SessionId = "SessionId";
+ public const string ActivityName = "ActivityName";
+ public const string Error = "Error";
+ public const string ErrorDetail = "ErrorDetail";
+ public const string Id = "Id";
+ public const string UserId = "UserId";
+ public const string OrganizationId = "OrganizationId";
+ public const string IpAddress = "IpAddress";
+ public const string IsFirstSession = "IsFirstSession";
+ public const string DeviceId = "DeviceId";
+ public const string DeviceLanguage = "DeviceLanguage";
+ public const string OperatingSystem = "OperatingSystem";
+ public const string CountryIsoCode = "CountryIsoCode";
+ public const string InstalledSoftwares = "InstalledSoftwares";
+ public const string ControllerCount = "ControllerCount";
+ public const string EntityCount = "EntityCount";
+ public const string ProjectCount = "ProjectCount";
+ public const string ModuleCount = "ModuleCount";
+ public const string PermissionCount = "PermissionCount";
+ public const string AppServiceCount = "AppServiceCount";
+ public const string ProjectType = "ProjectType";
+ public const string ProjectId = "ProjectId";
+ public const string SolutionId = "SolutionId";
+ public const string Template = "Template";
+ public const string CreatedAbpStudioVersion = "CreatedAbpStudioVersion";
+ public const string IsTiered = "IsTiered";
+ public const string UiFramework = "UiFramework";
+ public const string DatabaseProvider = "DatabaseProvider";
+ public const string DatabaseManagementSystem = "DatabaseManagementSystem";
+ public const string IsSeparateTenantSchema = "IsSeparateTenantSchema";
+ public const string Theme = "Theme";
+ public const string ThemeStyle = "ThemeStyle";
+ public const string MobileFramework = "MobileFramework";
+ public const string HasPublicWebsite = "HasPublicWebsite";
+ public const string IncludeTests = "IncludeTests";
+ public const string MultiTenancy = "MultiTenancy";
+ public const string DynamicLocalization = "DynamicLocalization";
+ public const string KubernetesConfiguration = "KubernetesConfiguration";
+ public const string GrafanaDashboard = "GrafanaDashboard";
+ public const string SocialLogins = "SocialLogins";
+ public const string InstalledModules = "InstalledModules";
+ public const string SolutionPath = "SolutionPath";
+ public const string LicenseType = "LicenseType";
+ public const string SessionType = "SessionType";
+ public const string HasError = "HasError";
+ public const string ActivityDuration = "ActivityDuration";
+ public const string ActivityDetails = "ActivityDetails";
+ public const string Time = "Time";
+ public const string SoftwareName = "Name";
+ public const string SoftwareVersion = "Version";
+ public const string SoftwareUiTheme = "UiTheme";
+ public const string SoftwareType = "SoftwareType";
+ public const string WebFramework = "WebFramework";
+ public const string Dbms = "Dbms";
+ public const string UiTheme = "UiTheme";
+ public const string UiThemeStyle = "UiThemeStyle";
+ public const string MobileApp = "MobileApp";
+ public const string SampleCrudPage = "SampleCrudPage";
+ public const string FirstAbpVersion = "FirstAbpVersion";
+ public const string FirstDotnetVersion = "FirstDotnetVersion";
+ public const string CreationTool = "CreationTool";
+ public const string ModuleName = "ModuleName";
+ public const string ModuleVersion = "ModuleVersion";
+ public const string ModuleInstallationTime = "ModuleInstallationTime";
+ public const string ExtraProperties = "ExtraProperties";
+ public const string HasSolutionInfo = "HasSolutionInfo";
+ public const string HasDeviceInfo = "HasDeviceInfo";
+ public const string HasProjectInfo = "HasProjectInfo";
+ public const string ErrorMessage = "ErrorMessage";
+ public const string FailingActivity = "FailingActivity";
+ public const string OperatingSystemArchitecture = "OperatingSystemArchitecture";
+ public const string AdditionalProperties = "AdditionalProperties";
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs
new file mode 100644
index 0000000000..e3993886a0
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/DeviceManager.cs
@@ -0,0 +1,260 @@
+using System;
+
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+static internal class DeviceManager
+{
+ public static string GetUniquePhysicalKey(bool shouldHash)
+ {
+ char platformId = '?';
+ char osArchitecture = '?';
+ string operatingSystem = "?";
+
+ try
+ {
+ string osPrefix;
+ string uniqueKey;
+
+ platformId = GetPlatformIdOrDefault();
+ osArchitecture = GetOsArchitectureOrDefault();
+
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform
+ .Windows))
+ {
+ operatingSystem = "Windows";
+ uniqueKey = GetUniqueKeyForWindows();
+ osPrefix = "W";
+ }
+ else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices
+ .OSPlatform.Linux))
+ {
+ operatingSystem = "Linux";
+ uniqueKey = GetHarddiskSerialForLinux();
+ osPrefix = "L";
+ }
+ else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices
+ .OSPlatform.OSX)) //MAC
+ {
+ operatingSystem = "OSX";
+ uniqueKey = GetHarddiskSerialForOsX();
+ osPrefix = "O";
+ }
+ else
+ {
+ operatingSystem = "Other";
+ uniqueKey = GetNetworkAdapterSerial();
+ osPrefix = "X";
+ }
+
+ if (shouldHash)
+ {
+ uniqueKey = ConvertToMd5(uniqueKey).ToUpperInvariant();
+ }
+
+ return osPrefix + platformId + osArchitecture + "-" + uniqueKey;
+ }
+ catch
+ {
+ return Guid.NewGuid().ToString();
+ }
+ }
+
+ private static string GetNetworkAdapterSerial()
+ {
+ string macAddress = string.Empty;
+
+ var networkInterfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces();
+ foreach (var networkInterface in networkInterfaces)
+ {
+ if (networkInterface.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
+ {
+ continue;
+ }
+
+ var physicalAddress = networkInterface.GetPhysicalAddress().ToString();
+ if (string.IsNullOrEmpty(physicalAddress))
+ {
+ continue;
+ }
+
+ macAddress = physicalAddress;
+ break;
+ }
+
+ return macAddress!;
+ }
+
+ private static char GetPlatformIdOrDefault(char defaultValue = '*')
+ {
+ try
+ {
+ return ((int)System.Environment.OSVersion.Platform).ToString()[0];
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ private static string ConvertToMd5(string text)
+ {
+ using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider())
+ {
+ return EncodeBase64(md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text)));
+ }
+ }
+
+ private static string EncodeBase64(byte[] ba)
+ {
+ var hex = new System.Text.StringBuilder(ba.Length * 2);
+
+ foreach (var b in ba)
+ {
+ hex.AppendFormat("{0:x2}", b);
+ }
+
+ return hex.ToString();
+ }
+
+
+ private static char GetOsArchitectureOrDefault(char defaultValue = '*')
+ {
+ try
+ {
+ return ((int)System.Runtime.InteropServices.RuntimeInformation.OSArchitecture).ToString()[0];
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ private static string GetUniqueKeyForWindows()
+ {
+ try
+ {
+ return GetProcessorIdForWindows();
+ }
+ catch
+ {
+ }
+
+ return GetWindowsMachineUniqueId();
+ }
+
+ private static string GetProcessorIdForWindows()
+ {
+ using (var managementObjectSearcher =
+ new System.Management.ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor"))
+ {
+ using (var searcherObj = managementObjectSearcher.Get())
+ {
+ if (searcherObj.Count == 0)
+ {
+ throw new System.Exception("No unique computer ID found for this computer!");
+ }
+
+ var managementObjectEnumerator = searcherObj.GetEnumerator();
+ managementObjectEnumerator.MoveNext();
+ return managementObjectEnumerator.Current.GetPropertyValue("ProcessorId").ToString()!;
+ }
+ }
+ }
+
+ private static string GetWindowsMachineUniqueId()
+ {
+ return RunCommandAndGetOutput("powershell (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID");
+ }
+
+
+ private static string GetHarddiskSerialForLinux()
+ {
+ return RunCommandAndGetOutput(
+ "udevadm info --query=all --name=/dev/sda | grep ID_SERIAL_SHORT | tr -d \"ID_SERIAL_SHORT=:\"");
+ }
+
+ private static string GetHarddiskSerialForOsX()
+ {
+ var command =
+ "ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { split($0, line, \"\\\"\"); printf(\"%s\\n\", line[4]); }'";
+
+ command = System.Text.RegularExpressions.Regex.Replace(command, @"(\\*)" + "\"", @"$1$1\" + "\"");
+
+ return RunCommandAndGetOutput(command);
+ }
+
+ private static string RunCommandAndGetOutput(string command)
+ {
+ var output = "";
+
+ using (var process = new System.Diagnostics.Process())
+ {
+ process.StartInfo = new System.Diagnostics.ProcessStartInfo(GetFileName())
+ {
+ Arguments = GetArguments(command),
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+ process.Start();
+ process?.WaitForExit();
+
+ using (var stdOut = process!.StandardOutput)
+ {
+ using (var stdErr = process.StandardError)
+ {
+ output = stdOut.ReadToEnd();
+ output += stdErr.ReadToEnd();
+ }
+ }
+ }
+
+ return output.Trim();
+ }
+
+ private static string GetFileName()
+ {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
+ System.Runtime.InteropServices.OSPlatform.OSX) ||
+ System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform
+ .Linux))
+ {
+ string[] fileNames = { "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh" };
+ foreach (var fileName in fileNames)
+ {
+ try
+ {
+ if (System.IO.File.Exists(fileName))
+ {
+ return fileName;
+ }
+ }
+ catch
+ {
+ //ignore
+ }
+ }
+
+ return "/bin/bash";
+ }
+
+ //Windows default.
+ return "cmd.exe";
+ }
+
+ private static string GetArguments(string command)
+ {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
+ System.Runtime.InteropServices.OSPlatform.OSX) ||
+ System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform
+ .Linux))
+ {
+ return "-c \"" + command + "\"";
+ }
+
+ //Windows default.
+ return "/C \"" + command + "\"";
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs
new file mode 100644
index 0000000000..f2b20e3a03
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/AbpTool.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+public enum AbpTool : byte
+{
+ Unknown = 0,
+ StudioUI = 1,
+ StudioCli = 2,
+ OldCli = 3
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs
new file mode 100644
index 0000000000..312eccec69
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/OperationSystem.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+public enum OperationSystem
+{
+ Unknown = 0,
+ Windows = 1,
+ MacOS = 2,
+ Linux = 3,
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs
new file mode 100644
index 0000000000..568fdb4f4d
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SessionType.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+public enum SessionType
+{
+ Unknown = 0,
+ AbpStudio = 1,
+ AbpCli = 2,
+ ApplicationRuntime = 3
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs
new file mode 100644
index 0000000000..0e334513da
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/Enums/SoftwareType.cs
@@ -0,0 +1,11 @@
+namespace Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+public enum SoftwareType : byte
+{
+ Others = 0,
+ AbpStudio = 1,
+ DotnetSdk = 2,
+ OperatingSystem = 3,
+ Ide = 4,
+ Browser = 5
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs
new file mode 100644
index 0000000000..c2797efd4a
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryConsts.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+public class TelemetryConsts
+{
+ public const string VoloNameSpaceFilter = "Volo.";
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs
new file mode 100644
index 0000000000..07bc408043
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/TelemetryPaths.cs
@@ -0,0 +1,13 @@
+using System;
+using System.IO;
+
+namespace Volo.Abp.Internal.Telemetry.Constants;
+
+public static class TelemetryPaths
+{
+ public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin");
+ public static string ComputerId => Path.Combine(AbpRootPath, "cli", "computer-id.bin");
+ public static string ActivityStorage => Path.Combine(AbpRootPath , "telemetry", "activity-storage.bin");
+ public static string Studio => Path.Combine(AbpRootPath, "studio");
+ private readonly static string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp");
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs
new file mode 100644
index 0000000000..8942bf359d
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareDetector.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+internal interface ISoftwareDetector
+{
+ string Name { get; }
+ Task DetectAsync();
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs
new file mode 100644
index 0000000000..ae5d75a099
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/ISoftwareInfoProvider.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+internal interface ISoftwareInfoProvider
+{
+ Task> GetSoftwareInfoAsync();
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs
new file mode 100644
index 0000000000..bbfa3fe414
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Contracts/SoftwareInfo.cs
@@ -0,0 +1,11 @@
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+internal class SoftwareInfo(string name, string? version, string? uiTheme, SoftwareType softwareType)
+{
+ public string Name { get; set; } = name;
+ public string? Version { get; set; } = version;
+ public string? UiTheme { get; set; } = uiTheme;
+ public SoftwareType SoftwareType { get; set; } = softwareType;
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs
new file mode 100644
index 0000000000..6b02c22c7c
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Core/SoftwareDetector.cs
@@ -0,0 +1,78 @@
+using System.Diagnostics;
+using System.Text;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+[ExposeServices(typeof(ISoftwareDetector))]
+abstract internal class SoftwareDetector: ISoftwareDetector , ISingletonDependency
+{
+ public abstract string Name { get; }
+ public abstract Task DetectAsync();
+
+ protected virtual async Task ExecuteCommandAsync(string command, string? arg)
+ {
+ var outputBuilder = new StringBuilder();
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = command,
+ Arguments = arg ?? "",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = new Process();
+ process.StartInfo = processStartInfo;
+ process.EnableRaisingEvents = true;
+
+ var tcs = new TaskCompletionSource();
+
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (e.Data != null)
+ {
+ outputBuilder.AppendLine(e.Data);
+ }
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (e.Data != null)
+ {
+ outputBuilder.AppendLine(e.Data);
+ }
+ };
+
+ process.Exited += (sender, e) =>
+ {
+ tcs.TrySetResult(true);
+ };
+
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ await tcs.Task;
+
+ var output = outputBuilder.ToString().Trim();
+ return string.IsNullOrWhiteSpace(output) ? null : output;
+ }
+
+ protected string? GetFileVersion(string filePath)
+ {
+ try
+ {
+ var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
+ return versionInfo.FileVersion;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs
new file mode 100644
index 0000000000..3d35c7192a
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/AbpStudioDetector.cs
@@ -0,0 +1,76 @@
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class AbpStudioDetector : SoftwareDetector
+{
+ public override string Name => "Abp Studio";
+ private const string AbpStudioVersionExtensionName = "Volo.Abp.Studio.Extensions.StandardSolutionTemplates";
+
+ public override Task DetectAsync()
+ {
+ try
+ {
+ var uiTheme = GetAbpStudioUiTheme();
+ var version = GetAbpStudioVersion();
+
+ return Task.FromResult(new SoftwareInfo(Name, version, uiTheme, SoftwareType.AbpStudio));
+ }
+ catch
+ {
+ return Task.FromResult(null);
+ }
+ }
+
+ private string? GetAbpStudioUiTheme()
+ {
+ var ideStateJsonPath = Path.Combine(
+ TelemetryPaths.Studio,
+ "ui",
+ "ide-state.json"
+ );
+ if (!File.Exists(ideStateJsonPath))
+ {
+ return null;
+ }
+ using var fs = new FileStream(ideStateJsonPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var doc = JsonDocument.Parse(fs);
+
+ return doc.RootElement.TryGetProperty("theme", out var themeElement) ? themeElement.GetString() : null;
+ }
+
+ private string? GetAbpStudioVersion()
+ {
+ var extensionsFilePath = Path.Combine(TelemetryPaths.Studio, "extensions.json");
+
+ if (!File.Exists(extensionsFilePath))
+ {
+ return null;
+ }
+
+ using var fs = new FileStream(extensionsFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var doc = JsonDocument.Parse(fs);
+
+ if (doc.RootElement.TryGetProperty("Extensions", out var extensionsElement) &&
+ extensionsElement.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var extension in extensionsElement.EnumerateArray())
+ {
+ if (extension.TryGetProperty("name", out var nameProp) &&
+ nameProp.GetString() == AbpStudioVersionExtensionName &&
+ extension.TryGetProperty("version", out var versionProp))
+ {
+ return versionProp.GetString();
+ }
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs
new file mode 100644
index 0000000000..fa7f8cff74
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/ChromeDetector.cs
@@ -0,0 +1,48 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class ChromeDetector : SoftwareDetector
+{
+ public override string Name => "Chrome";
+
+ public async override Task DetectAsync()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var chromePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Google", "Chrome", "Application", "chrome.exe");
+ if (File.Exists(chromePath))
+ {
+ return new SoftwareInfo(Name, GetFileVersion(chromePath), null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
+ if (File.Exists(chromePath))
+ {
+ var version = await ExecuteCommandAsync(chromePath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ var chromePath = "/usr/bin/google-chrome";
+ if (File.Exists(chromePath))
+ {
+ var version = await ExecuteCommandAsync(chromePath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+
+ return null;
+ }
+
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs
new file mode 100644
index 0000000000..4f03eac248
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/DotnetSdkDetector.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class DotnetSdkDetector : SoftwareDetector
+{
+ public override string Name => "DotnetSdk";
+
+ public async override Task DetectAsync()
+ {
+ return new SoftwareInfo(Name, Environment.Version.ToString(), null, SoftwareType.DotnetSdk);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs
new file mode 100644
index 0000000000..261e36c7e5
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/FireFoxDetector.cs
@@ -0,0 +1,45 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class FireFoxDetector : SoftwareDetector
+{
+ public override string Name => "Firefox";
+ public async override Task DetectAsync()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Mozilla Firefox", "firefox.exe");
+ if (File.Exists(firefoxPath))
+ {
+ return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var firefoxPath = "/Applications/Firefox.app/Contents/MacOS/firefox";
+ if (File.Exists(firefoxPath))
+ {
+ var version = await ExecuteCommandAsync(firefoxPath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ var firefoxPath = "/usr/bin/firefox";
+ if (File.Exists(firefoxPath))
+ {
+ var version = await ExecuteCommandAsync(firefoxPath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs
new file mode 100644
index 0000000000..e1e67c516e
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/MsEdgeDetector.cs
@@ -0,0 +1,47 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class MsEdgeDetector : SoftwareDetector
+{
+ public override string Name => "MsEdge";
+ public async override Task DetectAsync()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
+ "Microsoft", "Edge", "Application", "msedge.exe");
+
+ if (File.Exists(firefoxPath))
+ {
+ return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var edgePath = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge";
+ if (File.Exists(edgePath))
+ {
+ var version = await ExecuteCommandAsync(edgePath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ var edgePath = "/usr/bin/microsoft-edge";
+ if (File.Exists(edgePath))
+ {
+ var version = await ExecuteCommandAsync(edgePath, "--version");
+ return new SoftwareInfo(Name, version, null, SoftwareType.Browser);
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs
new file mode 100644
index 0000000000..85a4e05cce
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/NodeJsDetector.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal class NodeJsDetector : SoftwareDetector
+{
+ public override string Name => "Node.js";
+
+ public async override Task DetectAsync()
+ {
+ try
+ {
+ var output = await ExecuteCommandAsync("node", "-v");
+
+ if (output.IsNullOrWhiteSpace())
+ {
+ return null;
+ }
+
+ var version = output.Trim().TrimStart('v');
+
+ return new SoftwareInfo(Name, version, uiTheme: null, SoftwareType.Others);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs
new file mode 100644
index 0000000000..9dfc77f8f2
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/OperatingSystemDetector.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class OperatingSystemDetector : SoftwareDetector
+{
+ public override string Name => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" :
+ RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macOS" : "Linux";
+
+ public async override Task DetectAsync()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return new SoftwareInfo(Name, Environment.OSVersion.Version.ToString(), null, SoftwareType.OperatingSystem);
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var version = await ExecuteCommandAsync("sw_vers", "-productVersion");
+ return new SoftwareInfo(Name, version, GetMacUiTheme(), SoftwareType.OperatingSystem);
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ var version = await ExecuteCommandAsync("lsb_release", "-ds") ?? await ExecuteCommandAsync("uname", "-r");
+ return new SoftwareInfo(Name, version, await GetLinuxUiTheme(), SoftwareType.OperatingSystem);
+ }
+
+ return null;
+ }
+
+ private async Task GetLinuxUiTheme()
+ {
+ var output = await ExecuteCommandAsync("gsettings", "get org.gnome.desktop.interface gtk-theme");
+
+ if (!output.IsNullOrWhiteSpace() && output.ToLowerInvariant().Contains("dark"))
+ {
+ return "Dark";
+ }
+
+ return "Light";
+ }
+
+
+ private string? GetMacUiTheme()
+ {
+ try
+ {
+ using var process = new Process();
+ process.StartInfo = new ProcessStartInfo
+ {
+ FileName = "defaults",
+ Arguments = "read -g AppleInterfaceStyle",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ process.Start();
+ var output = process.StandardOutput.ReadToEnd().Trim();
+ process.WaitForExit();
+
+ return output == "Dark" ? "Dark" : "Light";
+ }
+ catch
+ {
+ return "Light";
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs
new file mode 100644
index 0000000000..a36dd8a0ab
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/RiderDetector.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class RiderDetector : SoftwareDetector
+{
+ public override string Name => "Rider";
+
+ public override Task DetectAsync()
+ {
+ try
+ {
+ string baseConfigDir;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ baseConfigDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "JetBrains");
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ baseConfigDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.Personal),
+ "Library", "Application Support", "JetBrains");
+ }
+ else
+ {
+ baseConfigDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.Personal),
+ ".config", "JetBrains");
+ }
+
+ if (!Directory.Exists(baseConfigDir))
+ {
+ return Task.FromResult(null);
+ }
+
+ var riderDirs = Directory
+ .GetDirectories(baseConfigDir, "Rider*")
+ .Select(dir =>
+ {
+ var name = Path.GetFileName(dir);
+ var verStr = name.Substring("Rider".Length);
+ return Version.TryParse(verStr, out var v)
+ ? (Path: dir, Version: v)
+ : (Path: null, Version: null);
+ })
+ .Where(x => x.Path != null)
+ .ToList();
+
+ if (!riderDirs.Any())
+ {
+ return Task.FromResult(null);
+ }
+
+ var latest = riderDirs
+ .OrderByDescending(x => x.Version)
+ .First();
+
+ var theme = string.Empty;
+ var colorsFile = Path.Combine(latest.Path!, "options", "colors.scheme.xml");
+ if (File.Exists(colorsFile))
+ {
+ try
+ {
+ var doc = XDocument.Load(colorsFile);
+ var schemeEl = doc
+ .Descendants("global_color_scheme")
+ .FirstOrDefault();
+ var schemeName = schemeEl?.Attribute("name")?.Value;
+ if (!schemeName.IsNullOrEmpty())
+ {
+ theme = schemeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0
+ ? "Dark"
+ : "Light";
+ }
+ }
+ catch
+ {
+ //ignored
+ }
+ }
+
+ return Task.FromResult(new SoftwareInfo(Name, latest.Version?.ToString(), theme,
+ SoftwareType.Ide));
+ }
+ catch (Exception e)
+ {
+ return Task.FromResult(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs
new file mode 100644
index 0000000000..72fda0f500
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioCodeDetector.cs
@@ -0,0 +1,132 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class VisualStudioCodeDetector : SoftwareDetector
+{
+ public override string Name => "Visual Studio Code";
+
+ public async override Task DetectAsync()
+ {
+ string? installDir = null;
+ string? settingsPath = null;
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ var progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+ var candidates = new[]
+ {
+ Path.Combine(localAppData, "Programs", "Microsoft VS Code"),
+ Path.Combine(progFiles, "Microsoft VS Code")
+ };
+ installDir = candidates.FirstOrDefault(Directory.Exists);
+
+ settingsPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "Code", "User", "globalStorage" ,"storage.json"
+ );
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var app = "/Applications/Visual Studio Code.app";
+ if (Directory.Exists(app))
+ {
+ installDir = app;
+ }
+
+ settingsPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.Personal),
+ "Library", "Application Support", "Code", "User", "globalStorage", "storage.json"
+ );
+ }
+ else
+ {
+ var candidate = "/usr/share/code";
+ if (Directory.Exists(candidate))
+ {
+ installDir = candidate;
+ }
+
+ settingsPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.Personal),
+ ".config", "Code", "User", "globalStorage", "storage.json"
+ );
+ }
+
+ if (installDir == null)
+ {
+ return null;
+ }
+
+
+ Version? version = null;
+ var productJson = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
+ ? Path.Combine(installDir, "Contents", "Resources", "app", "product.json")
+ : Path.Combine(installDir, "resources", "app", "product.json");
+
+ if (File.Exists(productJson))
+ {
+ try
+ {
+ using var jsonDoc = JsonDocument.Parse(File.ReadAllText(productJson));
+ var root = jsonDoc.RootElement;
+ if (root.TryGetProperty("version", out var versionProp))
+ {
+ var versionStr = versionProp.GetString();
+ if (Version.TryParse(versionStr, out var v))
+ {
+ version = v;
+ }
+ }
+ }
+ catch
+ {
+
+ }
+ }
+
+ if (version == null)
+ {
+ return null;
+ }
+
+ var theme = "Unknown";
+
+ if (File.Exists(settingsPath))
+ {
+ try
+ {
+ using var json = JsonDocument.Parse( File.ReadAllText(settingsPath));
+ var root = json.RootElement;
+ if (root.TryGetProperty("theme", out var themeProp))
+ {
+ var themeName = themeProp.GetString() ?? "";
+ if (themeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ theme = "Dark";
+ }
+ else if (themeName.IndexOf("light", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ theme = "Light";
+ }
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ return new SoftwareInfo(Name, version?.ToString(), theme, SoftwareType.Ide);
+
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs
new file mode 100644
index 0000000000..fbbb596157
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Detectors/VisualStudioDetector.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors;
+
+internal sealed class VisualStudioDetector : SoftwareDetector
+{
+ public override string Name => "Visual Studio";
+
+ public override Task DetectAsync()
+ {
+ var version = GetVisualStudioVersionViaVsWhere();
+ var theme = GetVisualStudioTheme();
+
+ if (version == null)
+ {
+ return Task.FromResult(null);
+ }
+
+ return Task.FromResult(new SoftwareInfo(
+ name: Name,
+ version: version,
+ uiTheme: theme,
+ softwareType: SoftwareType.Ide));
+ }
+
+ private string? GetVisualStudioVersionViaVsWhere()
+ {
+ var vswherePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
+ "Microsoft Visual Studio",
+ "Installer",
+ "vswhere.exe");
+
+ if (!File.Exists(vswherePath))
+ {
+ return null;
+ }
+
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = vswherePath,
+ Arguments = "-latest -property catalog_productDisplayVersion",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+
+ process.Start();
+ var output = process.StandardOutput.ReadToEnd().Trim();
+ process.WaitForExit();
+
+ return string.IsNullOrWhiteSpace(output) ? null : output;
+ }
+
+ private string? GetVisualStudioTheme()
+ {
+ var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+
+ var vsSettingsDir = Path.Combine(localAppData, "Microsoft", "VisualStudio");
+
+ if (!Directory.Exists(vsSettingsDir))
+ {
+ return null;
+ }
+
+ var settingsPath = Directory.GetFiles(vsSettingsDir, "CurrentSettings*.vssettings", SearchOption.AllDirectories)
+ .OrderByDescending(File.GetLastWriteTime)
+ .FirstOrDefault();
+
+ if (string.IsNullOrEmpty(settingsPath))
+ {
+ return null;
+ }
+
+ try
+ {
+ var doc = XDocument.Load(settingsPath);
+
+ var themeId = doc.Descendants("Theme")
+ .FirstOrDefault()?.Attribute("Id")?.Value;
+
+ return themeId?.ToUpperInvariant() switch
+ {
+ "{1DED0138-47CE-435E-84EF-9EC1F439B749}" => "Dark",
+ "{DE3DBBCD-F642-433C-8353-8F1DF4370ABA}" => "Light",
+ "{2DED0138-47CE-435E-84EF-9EC1F439B749}" => "Blue",
+ _ => "Unknown"
+ };
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs
new file mode 100644
index 0000000000..3a56ed420e
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/EnvironmentInspection/Providers/SoftwareInfoProvider.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts;
+
+namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Providers;
+
+internal class SoftwareInfoProvider : ISoftwareInfoProvider , ISingletonDependency
+{
+ private readonly IEnumerable _softwareDetectors;
+
+ public SoftwareInfoProvider(IEnumerable softwareDetectors)
+ {
+ _softwareDetectors = softwareDetectors;
+ }
+
+ public async Task> GetSoftwareInfoAsync()
+ {
+ var result = new List();
+
+ foreach (var softwareDetector in _softwareDetectors)
+ {
+ try
+ {
+ var softwareInfo = await softwareDetector.DetectAsync();
+ if (softwareInfo is not null)
+ {
+ result.Add(softwareInfo);
+ }
+ }
+ catch
+ {
+ //ignored
+ }
+ }
+ return result;
+ }
+
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs
new file mode 100644
index 0000000000..5f505324b0
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpPackageMetadataReader.cs
@@ -0,0 +1,135 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+
+namespace Volo.Abp.Internal.Telemetry.Helpers;
+
+static internal class AbpProjectMetadataReader
+{
+ private const string AbpPackageSearchPattern = "*.abppkg";
+ private const string AbpSolutionSearchPattern = "*.abpsln";
+ private const int MaxDepth = 5;
+ public static AbpProjectMetaData? ReadProjectMetadata(Assembly assembly)
+ {
+ var assemblyPath = assembly.Location;
+ try
+ {
+ var projectDirectory = Path.GetDirectoryName(assemblyPath);
+ if (projectDirectory == null)
+ {
+ return null;
+ }
+
+ var abpPackagePath = FindFileUpwards(projectDirectory, AbpPackageSearchPattern);
+
+ if (abpPackagePath.IsNullOrEmpty())
+ {
+ return null;
+ }
+
+ var projectMetaData = ReadOrCreateMetadata(abpPackagePath);
+
+ var abpSolutionPath = FindFileUpwards(projectDirectory, AbpSolutionSearchPattern);
+
+ if (!abpSolutionPath.IsNullOrEmpty())
+ {
+ projectMetaData.AbpSlnPath = abpSolutionPath;
+ }
+
+ return projectMetaData;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static AbpProjectMetaData ReadOrCreateMetadata(string packagePath)
+ {
+
+ var fileContent = File.ReadAllText(packagePath);
+ var metadata = new AbpProjectMetaData();
+
+ using var document = JsonDocument.Parse(fileContent);
+ var root = document.RootElement;
+
+ if (TryGetProjectId(root,out var projectId))
+ {
+ metadata.ProjectId = projectId;
+ }
+ else
+ {
+ metadata.ProjectId = Guid.NewGuid();
+ WriteProjectIdToPackageFile(root, packagePath, metadata.ProjectId.Value);
+ }
+
+ if (root.TryGetProperty("role", out var roleElement) &&
+ roleElement.ValueKind == JsonValueKind.String)
+ {
+ metadata.Role = roleElement.GetString()!;
+ }
+
+ return metadata;
+ }
+
+ private static void WriteProjectIdToPackageFile(JsonElement root, string packagePath, Guid projectId)
+ {
+ using var stream = new MemoryStream();
+ using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
+ {
+ writer.WriteStartObject();
+
+ if (root.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var property in root.EnumerateObject())
+ {
+ if (property.Name != "projectId")
+ {
+ property.WriteTo(writer);
+ }
+ }
+ }
+
+ writer.WriteString("projectId", projectId.ToString());
+ writer.WriteEndObject();
+ }
+
+ var json = Encoding.UTF8.GetString(stream.ToArray());
+ File.WriteAllText(packagePath, json);
+ }
+
+ private static string? FindFileUpwards(string startingDir, string searchPattern)
+ {
+ var currentDir = new DirectoryInfo(startingDir);
+ var currentDepth = 0;
+
+ while (currentDir != null && currentDepth < MaxDepth)
+ {
+ var file = currentDir.GetFiles(searchPattern).FirstOrDefault();
+ if (file != null)
+ {
+ return file.FullName;
+ }
+
+ currentDir = currentDir.Parent;
+ currentDepth++;
+ }
+
+ return null;
+ }
+ private static bool TryGetProjectId(JsonElement element, out Guid projectId)
+ {
+ if (element.TryGetProperty("projectId", out var projectIdElement) &&
+ projectIdElement.ValueKind == JsonValueKind.String &&
+ Guid.TryParse(projectIdElement.GetString(), out projectId))
+ {
+ return true;
+ }
+
+ projectId = Guid.Empty;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs
new file mode 100644
index 0000000000..d56ecf6106
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/AbpProjectMetaData.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Volo.Abp.Internal.Telemetry.Helpers;
+
+internal class AbpProjectMetaData
+{
+ public Guid? ProjectId { get; set; }
+ public string? Role { get; set; }
+ public string? AbpSlnPath { get; set; }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs
new file mode 100644
index 0000000000..3d402b651b
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/Cryptography.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Volo.Abp.Internal.Telemetry.Helpers;
+
+static internal class Cryptography
+{
+ private const string EncryptionKey = "AbpTelemetryStorageKey";
+
+ public static string Encrypt(string plainText)
+ {
+ Check.NotNullOrEmpty(plainText, nameof(plainText));
+ using var aes = Aes.Create();
+ using var sha256 = SHA256.Create();
+
+ aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey));
+ aes.Mode = CipherMode.ECB;
+ aes.Padding = PaddingMode.PKCS7;
+
+ var encryptor = aes.CreateEncryptor();
+ var inputBytes = Encoding.UTF8.GetBytes(plainText);
+ var encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
+ return Convert.ToBase64String(encryptedBytes);
+ }
+
+ public static string Decrypt(string cipherText)
+ {
+ Check.NotNullOrEmpty(cipherText, nameof(cipherText));
+ using var aes = Aes.Create();
+ using var sha256 = SHA256.Create();
+
+ aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey));
+ aes.Mode = CipherMode.ECB;
+ aes.Padding = PaddingMode.PKCS7;
+
+ var decryptor = aes.CreateDecryptor();
+ var inputBytes = Convert.FromBase64String(cipherText);
+ var decryptedBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
+ return Encoding.UTF8.GetString(decryptedBytes);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs
new file mode 100644
index 0000000000..d8eb28b40e
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Helpers/MutexExecutor.cs
@@ -0,0 +1,46 @@
+using System;
+using System.IO;
+using System.Threading;
+
+namespace Volo.Abp.Internal.Telemetry.Helpers;
+
+static internal class MutexExecutor
+{
+ private const string MutexName = "Global\\MyFileReadMutex";
+ private const int TimeoutMilliseconds = 3000;
+
+ public static string? ReadFileSafely(string filePath)
+ {
+ using var mutex = new Mutex(false, MutexName);
+
+ if (!mutex.WaitOne(TimeoutMilliseconds))
+ {
+ return null;
+ }
+
+ try
+ {
+ if (!File.Exists(filePath))
+ {
+ return null;
+ }
+
+ return File.ReadAllText(filePath);
+ }
+ catch (IOException)
+ {
+ return null;
+ }
+ finally
+ {
+ try
+ {
+ mutex.ReleaseMutex();
+ }
+ catch
+ {
+ // Already released or abandoned
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs
new file mode 100644
index 0000000000..46171de7ad
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryActivitySender.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry;
+
+public interface ITelemetryActivitySender
+{
+ Task TrySendQueuedActivitiesAsync();
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs
new file mode 100644
index 0000000000..32ec6f706a
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/ITelemetryService.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Internal.Telemetry;
+public interface ITelemetryService
+{
+ IAsyncDisposable TrackActivityAsync(string activityName, Action>? additionalProperties = null);
+ Task AddActivityAsync(string activityName, Action>? additionalProperties = null);
+ Task AddErrorActivityAsync(Action> additionalProperties);
+ Task AddErrorActivityAsync(string errorMessage);
+ Task AddErrorForActivityAsync(string failingActivity, string errorMessage);
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs
new file mode 100644
index 0000000000..d5efc1ccfd
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryActivitySender.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Internal.Telemetry;
+
+public class TelemetryActivitySender : ITelemetryActivitySender, IScopedDependency
+{
+ private readonly ITelemetryActivityStorage _telemetryActivityStorage;
+ private readonly ILogger _logger;
+
+ private const int ActivitySendBatchSize = 50;
+ private const int MaxRetryAttempts = 3;
+ private const int RetryDelayMilliseconds = 1000;
+
+ public TelemetryActivitySender(ITelemetryActivityStorage telemetryActivityStorage, ILogger logger)
+ {
+ _telemetryActivityStorage = telemetryActivityStorage;
+ _logger = logger;
+ }
+
+ public async Task TrySendQueuedActivitiesAsync()
+ {
+ if (!_telemetryActivityStorage.ShouldSendActivities())
+ {
+ return;
+ }
+
+ await SendActivitiesAsync();
+ }
+
+
+ private async Task SendActivitiesAsync()
+ {
+ try
+ {
+ var activities = _telemetryActivityStorage.GetActivities();
+ var batches = CreateActivityBatches(activities);
+
+ using var httpClient = new HttpClient();
+ ConfigureHttpClientAuthentication(httpClient);
+
+ foreach (var batch in batches)
+ {
+ var isSuccessful = await TrySendBatchWithRetriesAsync(httpClient, batch);
+
+ if (!isSuccessful)
+ {
+ break;
+ }
+ }
+ }
+ catch
+ {
+ //ignored
+ }
+ }
+
+
+ private async Task TrySendBatchWithRetriesAsync(HttpClient httpClient, ActivityEvent[] activities)
+ {
+ var currentAttempt = 0;
+
+ while (currentAttempt < MaxRetryAttempts)
+ {
+ try
+ {
+ var response = await httpClient.PostAsync($"{AbpPlatformUrls.AbpTelemetryApiUrl}api/telemetry/collect", new StringContent(JsonSerializer.Serialize(activities), Encoding.UTF8, "application/json"));
+
+ if (response.IsSuccessStatusCode)
+ {
+ _telemetryActivityStorage.DeleteActivities(activities);
+ }
+ else
+ {
+ _logger.LogWithLevel(LogLevel.Trace,
+ $"Failed to send telemetry activities. Status code: {response.StatusCode}, Reason: {response.ReasonPhrase}");
+ _telemetryActivityStorage.MarkActivitiesAsFailed(activities);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWithLevel(LogLevel.Trace, $"Error sending telemetry activities: {ex.Message}");
+ currentAttempt++;
+ await Task.Delay(currentAttempt * RetryDelayMilliseconds);
+ }
+ }
+
+ _logger.LogWithLevel(LogLevel.Trace, "Max retries reached. Failed to send telemetry activities.");
+
+ return false;
+ }
+
+ private static IEnumerable CreateActivityBatches(List activities)
+ {
+ return activities
+ .Select((x, i) => new { Index = i, Value = x })
+ .GroupBy(x => x.Index / ActivitySendBatchSize)
+ .Select(x => x.Select(v => v.Value).ToArray());
+ }
+
+ private static void ConfigureHttpClientAuthentication(HttpClient httpClient)
+ {
+ if (!File.Exists(TelemetryPaths.AccessToken))
+ {
+ return;
+ }
+
+ var accessToken = File.ReadAllText(TelemetryPaths.AccessToken, Encoding.UTF8);
+
+ if (accessToken.IsNullOrEmpty())
+ {
+ return;
+ }
+
+ httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
new file mode 100644
index 0000000000..8a2e3b4382
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Constants;
+using ActivityContext = Volo.Abp.Internal.Telemetry.Activity.ActivityContext;
+
+namespace Volo.Abp.Internal.Telemetry;
+
+public class TelemetryService : ITelemetryService, IScopedDependency
+{
+ private readonly ITelemetryActivitySender _telemetryActivitySender;
+ private readonly ITelemetryActivityEventBuilder _telemetryActivityEventBuilder;
+ private readonly ITelemetryActivityStorage _telemetryActivityStorage;
+
+ public TelemetryService(ITelemetryActivitySender telemetryActivitySender,
+ ITelemetryActivityEventBuilder telemetryActivityEventBuilder,
+ ITelemetryActivityStorage telemetryActivityStorage)
+ {
+ _telemetryActivitySender = telemetryActivitySender;
+ _telemetryActivityEventBuilder = telemetryActivityEventBuilder;
+ _telemetryActivityStorage = telemetryActivityStorage;
+ }
+
+
+ public IAsyncDisposable TrackActivityAsync(string activityName,
+ Action>? additionalProperties = null)
+ {
+ Check.NotNullOrEmpty(activityName, nameof(activityName));
+ var stopwatch = Stopwatch.StartNew();
+ var context = ActivityContext.Create(activityName, additionalProperties: additionalProperties);
+
+ return new AsyncDisposeFunc(async () =>
+ {
+ stopwatch.Stop();
+ context.Current[ActivityPropertyNames.ActivityDuration] = stopwatch.ElapsedMilliseconds;
+ await AddActivityAsync(context);
+ });
+ }
+
+ public async Task AddActivityAsync(string activityName,
+ Action>? additionalProperties = null)
+ {
+ Check.NotNullOrEmpty(activityName, nameof(activityName));
+ var context = ActivityContext.Create(activityName, additionalProperties: additionalProperties);
+ await AddActivityAsync(context);
+ }
+
+ public async Task AddErrorActivityAsync(Action> additionalProperties)
+ {
+ var context = ActivityContext.Create(ActivityNameConsts.Error, additionalProperties: additionalProperties);
+ await AddActivityAsync(context);
+ }
+
+ public async Task AddErrorActivityAsync(string errorMessage)
+ {
+ var context = ActivityContext.Create(ActivityNameConsts.Error, errorMessage);
+ await AddActivityAsync(context);
+ }
+
+ public async Task AddErrorForActivityAsync(string failingActivity, string errorMessage)
+ {
+ Check.NotNullOrEmpty(failingActivity, nameof(failingActivity));
+ var context = ActivityContext.Create(ActivityNameConsts.Error, errorMessage, configure =>
+ {
+ configure[ActivityPropertyNames.FailingActivity] = failingActivity;
+ });
+ await AddActivityAsync(context);
+ }
+
+ private Task AddActivityAsync(ActivityContext context)
+ {
+ _ = Task.Run(async () =>
+ {
+ await BuildAndSendActivityAsync(context);
+ });
+
+ return Task.CompletedTask;
+ }
+
+ private async Task BuildAndSendActivityAsync(ActivityContext context)
+ {
+ try
+ {
+ var activityEvent = await _telemetryActivityEventBuilder.BuildAsync(context);
+ if (activityEvent is null)
+ {
+ return;
+ }
+
+ _telemetryActivityStorage.SaveActivity(activityEvent);
+ await _telemetryActivitySender.TrySendQueuedActivitiesAsync();
+ }
+ catch
+ {
+ //ignored
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
index 06d2ee885b..0118fd9874 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
@@ -80,6 +80,37 @@ public static class TypeHelper
return false;
}
+ public static TProperty? ChangeTypePrimitiveExtended(object? value)
+ {
+ if (value == null)
+ {
+ return default;
+ }
+
+ if (IsPrimitiveExtended(typeof(TProperty), includeEnums: true))
+ {
+ var conversionType = typeof(TProperty);
+ if (IsNullable(conversionType))
+ {
+ conversionType = conversionType.GetFirstGenericArgumentIfNullable();
+ }
+
+ if (conversionType == typeof(Guid))
+ {
+ return (TProperty)TypeDescriptor.GetConverter(conversionType).ConvertFromInvariantString(value.ToString()!)!;
+ }
+
+ if (conversionType.IsEnum)
+ {
+ return (TProperty)Enum.Parse(conversionType, value.ToString()!);
+ }
+
+ return (TProperty)Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture);
+ }
+
+ throw new AbpException("ChangeTypePrimitiveExtended does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually.");
+ }
+
public static bool IsNullable(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
@@ -176,6 +207,10 @@ public static class TypeHelper
type == typeof(decimal) ||
type == typeof(DateTime) ||
type == typeof(DateTimeOffset) ||
+#if NETCOREAPP
+ type == typeof(DateOnly) ||
+ type == typeof(TimeOnly) ||
+#endif
type == typeof(TimeSpan) ||
type == typeof(Guid);
}
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs
index cd85acce50..32c0cc71bf 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs
@@ -43,10 +43,19 @@ public abstract class BasicRepositoryBase :
public IEntityChangeTrackingProvider EntityChangeTrackingProvider => LazyServiceProvider.LazyGetRequiredService();
public bool? IsChangeTrackingEnabled { get; protected set; }
-
- protected BasicRepositoryBase()
+
+ public string? EntityName { get; set; }
+
+ public void SetEntityName(string? name)
{
+ EntityName = name;
+ }
+ public string ProviderName { get; }
+
+ protected BasicRepositoryBase(string providerName)
+ {
+ ProviderName = Check.NotNullOrWhiteSpace(providerName, nameof(providerName));
}
public abstract Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default);
@@ -139,6 +148,12 @@ public abstract class BasicRepositoryBase :
public abstract class BasicRepositoryBase : BasicRepositoryBase, IBasicRepository
where TEntity : class, IEntity
{
+ protected BasicRepositoryBase(string providerName)
+ : base(providerName)
+ {
+
+ }
+
public virtual async Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
var entity = await FindAsync(id, includeDetails, cancellationToken);
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
index dc39255b25..cadc25da43 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
@@ -8,11 +8,15 @@ using Volo.Abp.Domain.Entities;
namespace Volo.Abp.Domain.Repositories;
///
-/// Just to mark a class as repository.
+/// The base interface to implement a repository for an entity.
///
public interface IRepository
{
bool? IsChangeTrackingEnabled { get; }
+
+ string? EntityName { get; set; }
+
+ string ProviderName { get; }
}
public interface IRepository : IReadOnlyRepository, IBasicRepository
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
index eb2457f8c8..1277b9ceb8 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
@@ -14,6 +14,11 @@ namespace Volo.Abp.Domain.Repositories;
public abstract class RepositoryBase : BasicRepositoryBase, IRepository, IUnitOfWorkManagerAccessor
where TEntity : class, IEntity
{
+ protected RepositoryBase(string providerName)
+ : base(providerName)
+ {
+ }
+
[Obsolete("Use WithDetailsAsync method.")]
public virtual IQueryable WithDetails()
{
@@ -92,6 +97,11 @@ public abstract class RepositoryBase : BasicRepositoryBase, IR
public abstract class RepositoryBase : RepositoryBase, IRepository
where TEntity : class, IEntity
{
+ protected RepositoryBase(string providerName)
+ : base(providerName)
+ {
+ }
+
public abstract Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default);
public abstract Task FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default);
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs
index 3c43798c38..70e69490a4 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs
@@ -250,4 +250,13 @@ public static class RepositoryExtensions
hardDeleteEntities.Add(entity);
await repository.DeleteAsync(entity, autoSave, cancellationToken);
}
+
+ public static TRepository SetEntityName(
+ this TRepository repository,
+ string name
+ ) where TRepository : class, IRepository
+ {
+ repository.EntityName = name;
+ return repository;
+ }
}
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs
new file mode 100644
index 0000000000..543cf8e406
--- /dev/null
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Telemetry/TelemetryDomainInfoEnricher.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Internal.Telemetry.Activity;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.Domain.Telemetry;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher))]
+public class TelemetryDomainInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher
+{
+ private readonly ITypeFinder _typeFinder;
+
+ public TelemetryDomainInfoEnricher(ITypeFinder typeFinder, IServiceProvider serviceProvider)
+ : base(serviceProvider)
+ {
+ _typeFinder = typeFinder;
+ }
+
+ protected override Task CanExecuteAsync(ActivityContext context)
+ {
+ return Task.FromResult(context.ProjectId.HasValue);
+ }
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ var entityCount = _typeFinder.Types.Count(t =>
+ typeof(IEntity).IsAssignableFrom(t) && !t.IsAbstract &&
+ !t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter));
+
+ context.Current[ActivityPropertyNames.EntityCount] = entityCount;
+
+ return Task.CompletedTask;
+ }
+
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
index 0ed9a2e199..575dccadef 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
+++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
@@ -61,7 +61,7 @@ public class EfCoreRepository : RepositoryBase, IE
}
[Obsolete("Use GetDbSetAsync() method.")]
- public virtual DbSet DbSet => DbContext.Set();
+ public virtual DbSet DbSet => GetDbSetInternal(DbContext);
Task> IEfCoreRepository.GetDbSetAsync()
{
@@ -70,7 +70,7 @@ public class EfCoreRepository : RepositoryBase, IE
protected async Task> GetDbSetAsync()
{
- return (await GetDbContextAsync()).Set();
+ return GetDbSetInternal(await GetDbContextAsync());
}
protected async Task GetDbConnectionAsync()
@@ -93,6 +93,7 @@ public class EfCoreRepository : RepositoryBase, IE
public IEfCoreBulkOperationProvider? BulkOperationProvider => LazyServiceProvider.LazyGetService();
public EfCoreRepository(IDbContextProvider dbContextProvider)
+ : base(AbpEfCoreConsts.ProviderName)
{
_dbContextProvider = dbContextProvider;
@@ -110,7 +111,7 @@ public class EfCoreRepository : RepositoryBase, IE
var dbContext = await GetDbContextAsync();
- var savedEntity = (await dbContext.Set().AddAsync(entity, GetCancellationToken(cancellationToken))).Entity;
+ var savedEntity = (await GetDbSetInternal(dbContext).AddAsync(entity, GetCancellationToken(cancellationToken))).Entity;
if (autoSave)
{
@@ -120,6 +121,13 @@ public class EfCoreRepository : RepositoryBase, IE
return savedEntity;
}
+ private DbSet GetDbSetInternal(TDbContext dbContext)
+ {
+ return EntityName != null
+ ? dbContext.Set(EntityName)
+ : dbContext.Set();
+ }
+
public async override Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entityArray = entities.ToArray();
@@ -147,7 +155,7 @@ public class EfCoreRepository : RepositoryBase, IE
return;
}
- await dbContext.Set().AddRangeAsync(entityArray, cancellationToken);
+ await GetDbSetInternal(dbContext).AddRangeAsync(entityArray, cancellationToken);
if (autoSave)
{
@@ -159,9 +167,10 @@ public class EfCoreRepository : RepositoryBase, IE
{
var dbContext = await GetDbContextAsync();
- if (dbContext.Set().Local.All(e => e != entity))
+ var dbSet = GetDbSetInternal(dbContext);
+ if (dbSet.Local.All(e => e != entity))
{
- dbContext.Set().Attach(entity);
+ dbSet.Attach(entity);
dbContext.Update(entity);
}
@@ -197,7 +206,7 @@ public class EfCoreRepository : RepositoryBase, IE
var dbContext = await GetDbContextAsync();
- dbContext.Set().UpdateRange(entityArray);
+ GetDbSetInternal(dbContext).UpdateRange(entityArray);
if (autoSave)
{
@@ -209,7 +218,7 @@ public class EfCoreRepository : RepositoryBase, IE
{
var dbContext = await GetDbContextAsync();
- dbContext.Set().Remove(entity);
+ GetDbSetInternal(dbContext).Remove(entity);
if (autoSave)
{
@@ -318,7 +327,7 @@ public class EfCoreRepository : RepositoryBase, IE
public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync();
- var dbSet = dbContext.Set();
+ var dbSet = GetDbSetInternal(dbContext);
var entities = await dbSet
.Where(predicate)
@@ -335,8 +344,9 @@ public class EfCoreRepository : RepositoryBase, IE
public async override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync();
- var dbSet = dbContext.Set();
- await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken));
+ await GetDbSetInternal(dbContext)
+ .Where(predicate)
+ .ExecuteDeleteAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task EnsureCollectionLoadedAsync(
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
index e91d08b8c9..fceb338051 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
+++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
@@ -37,6 +37,7 @@ using Volo.Abp.Reflection;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Volo.Abp.EntityFrameworkCore;
@@ -124,19 +125,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
TrySetDatabaseProvider(modelBuilder);
- foreach (var entityType in modelBuilder.Model.GetEntityTypes())
+ foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToArray())
{
- ConfigureBasePropertiesMethodInfo
- .MakeGenericMethod(entityType.ClrType)
- .Invoke(this, new object[] { modelBuilder, entityType });
-
- ConfigureValueConverterMethodInfo
- .MakeGenericMethod(entityType.ClrType)
- .Invoke(this, new object[] { modelBuilder, entityType });
-
- ConfigureValueGeneratedMethodInfo
- .MakeGenericMethod(entityType.ClrType)
- .Invoke(this, new object[] { modelBuilder, entityType });
+ ConfigureEntityTypeProperties(modelBuilder, entityType);
}
if (LazyServiceProvider == null || Options == null)
@@ -151,6 +142,23 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
}
}
+ protected virtual void ConfigureEntityTypeProperties(
+ ModelBuilder modelBuilder,
+ IMutableEntityType entityType)
+ {
+ ConfigureBasePropertiesMethodInfo
+ .MakeGenericMethod(entityType.ClrType)
+ .Invoke(this, new object[] { modelBuilder, entityType });
+
+ ConfigureValueConverterMethodInfo
+ .MakeGenericMethod(entityType.ClrType)
+ .Invoke(this, new object[] { modelBuilder, entityType });
+
+ ConfigureValueGeneratedMethodInfo
+ .MakeGenericMethod(entityType.ClrType)
+ .Invoke(this, new object[] { modelBuilder, entityType });
+ }
+
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
@@ -762,7 +770,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
AuditPropertySetter?.IncrementEntityVersionProperty(entry.Entity);
}
- protected virtual void ConfigureBaseProperties(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
+ protected virtual void ConfigureBaseProperties(
+ ModelBuilder modelBuilder,
+ IMutableEntityType mutableEntityType)
where TEntity : class
{
if (mutableEntityType.IsOwned())
@@ -775,54 +785,82 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
return;
}
- modelBuilder.Entity().ConfigureByConvention();
+ var entityTypeBuilder = CreateEntityTypeBuilderFromMutableEntityType(
+ modelBuilder,
+ mutableEntityType
+ );
+
+ entityTypeBuilder.ConfigureByConvention();
+
+ ConfigureGlobalFilters(modelBuilder, mutableEntityType, entityTypeBuilder);
+ }
- ConfigureGlobalFilters(modelBuilder, mutableEntityType);
+ protected virtual EntityTypeBuilder CreateEntityTypeBuilderFromMutableEntityType(
+ ModelBuilder modelBuilder,
+ IMutableEntityType mutableEntityType) where TEntity : class
+ {
+ return mutableEntityType.HasSharedClrType
+ ? modelBuilder.SharedTypeEntity(mutableEntityType.Name)
+ : modelBuilder.Entity();
}
- protected virtual void ConfigureGlobalFilters(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
+ protected virtual void ConfigureGlobalFilters(
+ ModelBuilder modelBuilder,
+ IMutableEntityType mutableEntityType,
+ EntityTypeBuilder entityTypeBuilder)
where TEntity : class
{
if (mutableEntityType.BaseType == null && ShouldFilterEntity(mutableEntityType))
{
- var filterExpression = CreateFilterExpression(modelBuilder);
+ var filterExpression = CreateFilterExpression(modelBuilder, entityTypeBuilder);
if (filterExpression != null)
{
- modelBuilder.Entity().HasAbpQueryFilter(filterExpression);
+ entityTypeBuilder.HasAbpQueryFilter(filterExpression);
}
}
}
- protected virtual void ConfigureValueConverter(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
+ protected virtual void ConfigureValueConverter(
+ ModelBuilder modelBuilder,
+ IMutableEntityType mutableEntityType)
where TEntity : class
{
- if (mutableEntityType.BaseType == null &&
- !typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) &&
- !typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) &&
- !mutableEntityType.IsOwned())
+ if (mutableEntityType.BaseType != null ||
+ typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) ||
+ typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) ||
+ mutableEntityType.IsOwned())
{
- if (LazyServiceProvider == null || Clock == null)
- {
- return;
- }
+ return;
+ }
- foreach (var property in mutableEntityType.GetProperties().
- Where(property => property.PropertyInfo != null &&
- (property.PropertyInfo.PropertyType == typeof(DateTime) || property.PropertyInfo.PropertyType == typeof(DateTime?)) &&
- property.PropertyInfo.CanWrite &&
- ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(property.PropertyInfo) == null))
- {
- modelBuilder
- .Entity()
- .Property(property.Name)
- .HasConversion(property.ClrType == typeof(DateTime)
- ? new AbpDateTimeValueConverter(Clock)
- : new AbpNullableDateTimeValueConverter(Clock));
- }
+ if (LazyServiceProvider == null || Clock == null)
+ {
+ return;
+ }
+
+
+ foreach (var property in mutableEntityType.GetProperties().
+ Where(property => property.PropertyInfo != null &&
+ (property.PropertyInfo.PropertyType == typeof(DateTime) || property.PropertyInfo.PropertyType == typeof(DateTime?)) &&
+ property.PropertyInfo.CanWrite &&
+ ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(property.PropertyInfo) == null))
+ {
+ var entityTypeBuilder = CreateEntityTypeBuilderFromMutableEntityType(
+ modelBuilder,
+ mutableEntityType
+ );
+
+ entityTypeBuilder
+ .Property(property.Name)
+ .HasConversion(property.ClrType == typeof(DateTime)
+ ? new AbpDateTimeValueConverter(Clock)
+ : new AbpNullableDateTimeValueConverter(Clock));
}
}
- protected virtual void ConfigureValueGenerated(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
+ protected virtual void ConfigureValueGenerated(
+ ModelBuilder modelBuilder,
+ IMutableEntityType mutableEntityType)
where TEntity : class
{
if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity)))
@@ -830,7 +868,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
return;
}
- var idPropertyBuilder = modelBuilder.Entity