Browse Source

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

pull/4554/head
liangshiwei 6 years ago
parent
commit
1fdd47609d
  1. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json
  2. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json
  3. 12
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  4. 6
      configureawait.props
  5. 1
      docs/en/CLI.md
  6. 63
      docs/en/Entity-Framework-Core-Oracle-Devart.md
  7. 58
      docs/en/Entity-Framework-Core-Oracle-Official.md
  8. 60
      docs/en/Entity-Framework-Core-Oracle.md
  9. 226
      docs/en/Tutorials/Part-1.md
  10. 576
      docs/en/Tutorials/Part-2.md
  11. BIN
      docs/en/Tutorials/images/bookstore-actions-buttons.png
  12. BIN
      docs/en/Tutorials/images/bookstore-angular-file-tree.png
  13. BIN
      docs/en/Tutorials/images/bookstore-book-list.png
  14. BIN
      docs/en/Tutorials/images/bookstore-confirmation-popup.png
  15. BIN
      docs/en/Tutorials/images/bookstore-empty-new-book-modal.png
  16. 114
      docs/en/UI/Angular/List-Service.md
  17. 370
      docs/en/UI/Angular/Migration-Guide-v3.md
  18. 216
      docs/en/UI/Angular/Modifying-the-Menu.md
  19. BIN
      docs/en/UI/Angular/images/table-of-packages-to-update.png
  20. 2
      docs/en/Unit-Of-Work.md
  21. 180
      docs/en/Virtual-File-System.md
  22. 4
      docs/en/docs-nav.json
  23. 21
      framework/Volo.Abp.sln
  24. 2
      framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj
  25. 27
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json
  26. 27
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Properties/launchSettings.json
  27. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json
  28. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs
  29. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Properties/launchSettings.json
  30. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Properties/launchSettings.json
  31. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Properties/launchSettings.json
  32. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Properties/launchSettings.json
  33. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyScriptContributor.cs
  34. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyStyleContributor.cs
  35. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Properties/launchSettings.json
  36. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj
  37. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj
  38. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs
  39. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml
  40. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml
  41. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Properties/launchSettings.json
  42. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj
  43. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Properties/launchSettings.json
  44. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Properties/launchSettings.json
  45. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo.Abp.AspNetCore.Mvc.UI.csproj
  46. 27
      framework/src/Volo.Abp.AspNetCore.Mvc/Properties/launchSettings.json
  47. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj
  48. 5
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
  49. 1
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
  50. 3
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  51. 15
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Authentication/ChallengeAccountController.cs
  52. 34
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs
  53. 46
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicMaxLengthAttributeAdapter.cs
  54. 54
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs
  55. 27
      framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json
  56. 2
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj
  57. 27
      framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
  58. 27
      framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json
  59. 2
      framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj
  60. 27
      framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json
  61. 2
      framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj
  62. 6
      framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj
  63. 6
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs
  64. 7
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorkerAdapter.cs
  65. 88
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs
  66. 18
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
  67. 2
      framework/src/Volo.Abp.BlobStoring.Azure/Volo.Abp.BlobStoring.Azure.csproj
  68. 1
      framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs
  69. 52
      framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobNamingNormalizer.cs
  70. 2
      framework/src/Volo.Abp.BlobStoring.FileSystem/Volo.Abp.BlobStoring.FileSystem.csproj
  71. 9
      framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs
  72. 41
      framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobNamingNormalizer.cs
  73. 62
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs
  74. 10
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs
  75. 24
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs
  76. 9
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizer.cs
  77. 3
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml
  78. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd
  79. 25
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj
  80. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs
  81. 294
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs
  82. 47
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs
  83. 2
      framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj
  84. 393
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
  85. 30
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs
  86. 63
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs
  87. 2
      framework/src/Volo.Abp.Castle.Core/Volo.Abp.Castle.Core.csproj
  88. 8
      framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
  89. 38
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs
  90. 47
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  91. 12
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
  92. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AngularEnvironmentFilePortChangeForSeparatedIdentityServersStep.cs
  93. 116
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  94. 49
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
  95. 7
      framework/src/Volo.Abp.Cli/Properties/launchSettings.json
  96. 4
      framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj
  97. 9
      framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs
  98. 23
      framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs
  99. 24
      framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj
  100. 2
      framework/src/Volo.Abp.Core/Volo/Abp/Collections/ITypeList.cs

3
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json

@ -8,6 +8,7 @@
"FrameworkDocumentation": "Framework documentation",
"OfficialBlog": "Official blog",
"CommercialHomePage": "Commercial home page",
"CommercialSupportWebSite": "Commercial support web site"
"CommercialSupportWebSite": "Commercial support web site",
"CommunityWebSite": "ABP community web site"
}
}

3
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json

@ -8,6 +8,7 @@
"FrameworkDocumentation": "Framework Dokümantasyon",
"OfficialBlog": "Resmi blog",
"CommercialHomePage": "Kurumsal ana sayfa",
"CommercialSupportWebSite": "Kurumsal destek web sitesi"
"CommercialSupportWebSite": "Kurumsal destek web sitesi",
"CommunityWebSite": "ABP topluluk web sitesi"
}
}

12
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -102,7 +102,16 @@
"DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules",
"OrganizationName": "Organization name",
"CreationDate": "Creation date",
"LicenseStart": "License start date",
"LicenseEndDate": "License end date",
"OrganizationNamePlaceholder": "Organization name...",
"TotalQuestionCountPlaceholder": "Total question count...",
"RemainingQuestionCountPlaceholder": "Remaining question count...",
"LicenseTypePlaceholder": "License type...",
"CreationDatePlaceholder": "Creation date...",
"LicenseStartDatePlaceholder": "License start date...",
"LicenseEndDatePlaceholder": "License end date...",
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
@ -156,6 +165,7 @@
"RemainingQuestionCount": "Remaining question count",
"TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !",
"QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !",
"UnlimitedQuestionCount": "Unlimited question count"
"UnlimitedQuestionCount": "Unlimited question count",
"Notes": "Notes"
}
}

6
configureawait.props

@ -1,8 +1,8 @@
<Project>
<ItemGroup>
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" />
<PackageReference Include="Fody" Version="6.0.6">
<PrivateAssets>all</PrivateAssets>
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" PrivateAssets="All" />
<PackageReference Include="Fody" Version="6.2.0">
<PrivateAssets>All</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

1
docs/en/CLI.md

@ -115,6 +115,7 @@ abp update [options]
* `--nuget`: Only updates NuGet packages.
* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default
* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default.
* `--check-all`: Check the new version of each package separately. Default is `false`.
### add-package

63
docs/en/Entity-Framework-Core-Oracle-Devart.md

@ -0,0 +1,63 @@
# Switch to EF Core Oracle Devart Provider
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
> This document uses a paid library of [Devart](https://www.devart.com/dotconnect/oracle/) company, See [this document](Entity-Framework-Core-Oracle.md) for other options.
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleDevartModule)`
Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`.
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.

58
docs/en/Entity-Framework-Core-Oracle-Official.md

@ -0,0 +1,58 @@
# Switch to EF Core Oracle Provider
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleModule)`
Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle;`.
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one (just changes `UseSqlServer(...)` to `UseOracle(...)`)
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseOracle(configuration.GetConnectionString("Default"));
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.

60
docs/en/Entity-Framework-Core-Oracle.md

@ -2,61 +2,9 @@
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
> This document uses a paid library of [Devart](https://www.devart.com/dotconnect/oracle/) company, because it is the only library for Oracle that supports EF Core 3.x.
ABP Framework provides integrations for two different Oracle packages. See one of the following documents based on your provider decision:
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.
* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** package uses the official & free oracle driver (which is **currently in beta**).
* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** package uses the commercial (paid) driver of [Devart](https://www.devart.com/) company.
> You can choose one of the package you want. If you don't know the differences of the packages, please search for it. ABP Framework only provides integrations it doesn't provide support for such 3rd-party libraries.

226
docs/en/Tutorials/Part-1.md

@ -754,8 +754,8 @@ It's end of this part. The final UI of this work is shown as below:
It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application:
- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services.
- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library.
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) will be used as the datatable library.
- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor).
#### Install NPM packages
@ -778,26 +778,56 @@ yarn ng generate module book --routing true
#### Routing
Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below
Open the `app-routing.module.ts` file in `src\app` folder and add a route as shown below:
```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
const routes: Routes = [
// ...
// added a new route to the routes array
{
path: 'books',
loadChildren: () => import('./book/book.module').then(m => m.BookModule)
}
]
```
//...added books path with the below to the routes array
{
path: 'books',
component: ApplicationLayoutComponent,
loadChildren: () => import('./book/book.module').then(m => m.BookModule),
data: {
routes: {
name: '::Menu:Books',
iconClass: 'fas fa-book'
} as ABP.Route
},
* We added a lazy-loaded route. See the [Lazy-Loading Feature Modules](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules).
Open the `route.provider.ts` file in `src\app` folder and replace the content as below:
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
//...
// added below element
{
path: '/books',
name: '::Menu:Books',
iconClass: 'fas fa-book',
order: 101,
layout: eLayoutType.application,
},
]);
};
}
```
* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item.
* We added a new route element to show a navigation element labeled "Books" on the menu.
* `path` is the URL of the route.
* `name` is the menu item name. A Localization key can be passed.
* `iconClass` is the icon of the menu item.
* `order` is the order of the menu item. We define 101 to show the route after the "Administration" item.
* `layout` is the layout of the BooksModule's routes. `eLayoutType.application`, `eLayoutType.account` or `eLayoutType.empty` can be defined.
For more information, see the [RoutesService document](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice).
#### Book list component
@ -865,35 +895,6 @@ Open the browser and navigate to http://localhost:4200/books. We'll see **book-l
![Initial book list page](./images/bookstore-initial-book-list-page.png)
#### Create BookState
Run the following command in the terminal to create a new state, named `BooksState`:
```bash
npx @ngxs/cli --name book --directory src/app/book
```
* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
```js
// ...
import { BookState } from './books/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
// ...
})
export class AppModule {}
```
#### Generate proxies
ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md)
@ -910,109 +911,41 @@ The generated files looks like below:
![Generated files](./images/generated-proxies.png)
#### GetBooks Action
Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions).
Open the `book.actions.ts` file in `app/book/state` folder and replace the content below:
```js
export class GetBooks {
static readonly type = '[Book] Get';
}
```
#### Implement BookState
Open the `book.state.ts` file in `app/book/state` folder and replace the content below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
}
```
* We added the book property to BookStateModel model.
* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI and patches the state.
* `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent
Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
loading = false;
constructor(private store: Store) {}
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
}
```
* We added the `get` function that updates store to get the books.
* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features.
* We imported and injected the generated `BookService`.
* We implemented the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service) that is a utility service to provide easy pagination, sorting, and search implementation.
Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below:
@ -1022,38 +955,31 @@ Open the `book-list.component.html` file in `app\book\book-list` folder and repl
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ "::Menu:Books" | abpLocalization }}}%}
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
</div>
</div>
<div class="card-body">
<abp-table
[value]="books$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
</div>
```
@ -1074,4 +1000,4 @@ In this tutorial we have applied the rules of official [Angular Style Guide](htt
### Next Part
See the [part 2](part-2.md) for creating, updating and deleting books.
See the [part 2](./Part-2.md) for creating, updating and deleting books.

576
docs/en/Tutorials/Part-2.md

@ -454,78 +454,50 @@ Run the application and try to delete a book.
In this section, you will learn how to create a new modal dialog form to create a new book.
#### State definitions
Open `book.action.ts` in `app\book\state` folder and replace the content as below:
#### Add a modal to BookListComponent
```js
import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Book] Get';
}
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto) { }
}
```
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from '../models';
import { BookService } from '../services';
* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action.
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
Open `book.state.ts` file in `app\book\state` folder and replace the content as below:
booksType = BookType;
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
isModalOpen = false; // <== added this line ==>
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
constructor(public readonly list: ListService, private bookService: BookService) {}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
book: bookResponse,
});
})
);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
// added CreateUpdateBook action listener
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
return this.bookService.createByInput(action.payload);
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book.
When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`.
* We defined a variable called `isModalOpen` and `createBook` method.
* We added the `createBook` method.
#### Add a modal to BookListComponent
Open `book-list.component.html` file in `books\book-list` folder and replace the content as below:
@ -534,19 +506,12 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
<div class="card-header">
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
<h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
</div>
<!--Added new book button -->
<!--Added new book button -->
<div class="text-right col col-md-6">
<div class="text-lg-right pt-2">
<button
id="create"
class="btn btn-primary"
type="button"
(click)="createBook()"
>
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button>
@ -555,47 +520,40 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
</div>
</div>
<div class="card-body">
<abp-table
[value]="books$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
</div>
<!--added modal-->
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template>
<ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template>
<ng-template #abpBody> </ng-template>
<ng-template #abpBody> </ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
</ng-template>
</abp-modal>
```
@ -603,55 +561,6 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
* We added `New book` button to the `AbpContentToolbar`.
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
loading = false;
isModalOpen = false; // <== added this line ==>
constructor(private store: Store) {}
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
}
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* We added `isModalOpen = false` and `createBook` method.
You can open your browser and click **New book** button to see the new modal.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
@ -663,48 +572,43 @@ You can open your browser and click **New book** button to see the new modal.
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
loading = false;
isModalOpen = false;
form: FormGroup; // <== added this line ==>
constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // <== injected FormBuilder ==>
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
this.buildForm(); //<== added this line ==>
this.buildForm(); // <== added this line ==>
this.isModalOpen = true;
}
@ -799,13 +703,10 @@ export class BookModule {}
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -813,37 +714,34 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==>
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
//added bookTypeArr array
// <== added bookTypeArr array ==>
bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {}
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
@ -884,13 +782,10 @@ Now, you can open your browser to see the changes:
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -898,11 +793,10 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
@ -910,24 +804,22 @@ export class BookListComponent implements OnInit {
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {}
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
@ -950,19 +842,18 @@ export class BookListComponent implements OnInit {
return;
}
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
this.bookService.createByInput(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
this.list.get();
});
}
}
```
* We imported `CreateUpdateBook`.
* We added `save` method
Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book.
Open `book-list.component.html` in `app\book\book-list` folder, find the `<ng-template #abpFooter>` element and replace this element with the following to create a new book.
```html
<ng-template #abpFooter>
@ -994,63 +885,24 @@ The final modal UI looks like below:
### Updating a book
#### CreateUpdateBook action
Open the `book.actions.ts` in `app\book\state` folder and replace the content as below:
```js
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* We added `id` parameter to the `CreateUpdateBook` action's constructor.
Open the `book.state.ts` in `app\book\state` folder and replace the `save` method as below:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
```
#### BookListComponent
Open `book-list.component.ts` in `app\book\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`.
Open `book-list.component.ts` in `app\book\book-list` folder and add a variable named `selectedBook`.
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../services'; // <== imported BookService ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
@ -1058,26 +910,24 @@ export class BookListComponent implements OnInit {
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
selectedBook = {} as BookDto; // <== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==>
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
// <== this method is replaced ==>
@ -1109,30 +959,31 @@ export class BookListComponent implements OnInit {
});
}
// <== this method is replaced ==>
save() {
if (this.form.invalid) {
return;
}
//<== added this.selectedBook.id ==>
this.store
.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
});
// <== added request ==>
const request = this.selectedBook.id
? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id)
: this.bookService.createByInput(this.form.value);
request.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
}
```
* We imported `BookService`.
* We declared a variable named `selectedBook` as `BookDto`.
* We injected `BookService` to the constructor. `BookService` is being used to retrieve the book data which is being edited.
* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object.
* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`.
* We replaced the `save` method.
#### Add "Actions" dropdown to the table
@ -1140,27 +991,14 @@ Open the `book-list.component.html` in `app\book\book-list` folder and replace
```html
<div class="card-body">
<abp-table
[value]="books$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Actions" | abpLocalization }}}%}</th>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<!-- added actions column -->
<ngx-datatable-column
[name]="'::Actions' | abpLocalization"
[maxWidth]="150"
[sortable]="false"
>
<ng-template let-row="row" ngx-datatable-cell-template>
<div ngbDropdown container="body" class="d-inline-block">
<button
class="btn btn-primary btn-sm dropdown-toggle"
@ -1168,25 +1006,37 @@ Open the `book-list.component.html` in `app\book\book-list` folder and replace
aria-haspopup="true"
ngbDropdownToggle
>
<i class="fa fa-cog mr-1"></i>{%{{{ "::Actions" | abpLocalization }}}%}
<i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(data.id)">
{%{{{ "::Edit" | abpLocalization }}}%}
<button ngbDropdownItem (click)="editBook(row.id)">
{%{{{ '::Edit' | abpLocalization }}}%}
</button>
</div>
</div>
</td>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ booksType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
```
- We added a `th` for the "Actions" column.
- We added a `ngx-datatable-column` for the "Actions" column.
- We added `button` with `ngbDropdownToggle` to open actions when clicked the button.
- We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions.
@ -1198,7 +1048,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng
```html
<ng-template #abpHeader>
<h3>{%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}</h3>
<h3>{%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}</h3>
</ng-template>
```
@ -1206,81 +1056,9 @@ Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng
### Deleting a book
#### DeleteBook action
Open `book.actions.ts` in `app\book\state` folder and add an action named `DeleteBook`.
```js
export class DeleteBook {
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
Open the `book.state.ts` in `app\book\state` folder and replace the content as below:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
```
- We imported `DeleteBook` .
- We added `DeleteBook` action listener to the end of the file.
#### Delete confirmation popup
Open `book-list.component.ts` in`app\book\book-list` folder and inject the `ConfirmationService`.
Open `book-list.component.ts` in `app\book\book-list` folder and inject the `ConfirmationService`.
Replace the constructor as below:
@ -1289,11 +1067,11 @@ import { ConfirmationService } from '@abp/ng.theme.shared';
//...
constructor(
private store: Store,
private fb: FormBuilder,
private bookService: BookService,
private confirmation: ConfirmationService // <== added this line ==>
) { }
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService // <== added this line ==>
) {}
```
* We imported `ConfirmationService`.
@ -1301,28 +1079,24 @@ constructor(
See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)
In the `book-list.component.ts` add a delete method :
In the `book-list.component.ts` add a delete method:
```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==>
//...
delete(id: string) {
this.confirmation
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
.subscribe(status => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
}
});
this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.deleteById(id).subscribe(() => this.list.get());
}
});
}
```
The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below:
The `delete` method shows a confirmation popup and subscribes for the user response. The `deleteById` method of `BookService` called only if user clicks to the `Yes` button. The confirmation popup looks like below:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
@ -1335,7 +1109,7 @@ Open `book-list.component.html` in `app\book\book-list` folder and modify the `n
```html
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)">
<button ngbDropdownItem (click)="delete(row.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>

BIN
docs/en/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/en/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Tutorials/images/bookstore-empty-new-book-modal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 56 KiB

114
docs/en/UI/Angular/List-Service.md

@ -53,59 +53,41 @@ class BookComponent {
}
```
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead.
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component members to expose the `ListService` instance instead.
Place `ListService` properties into the template like this:
Bind `ListService` to ngx-datatable like this:
```html
<abp-table
[value]="book.items"
[(page)]="list.page"
[rows]="list.maxResultCount"
[totalRecords]="book.totalCount"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[abpLoading]="list.isLoading$ | async"
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th (click)="nameSort.sort('name')">
{%{{{ '::Name' | abpLocalization }}}%}
<abp-sort-order-icon
#nameSort
sortKey="name"
[(selectedSortKey)]="list.sortKey"
[(order)]="list.sortOrder"
></abp-sort-order-icon>
</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
</tr>
</ng-template>
<!-- column templates here -->
</ngx-datatable>
```
## Usage with Observables
You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities:
```ts
```js
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query));
```
```html
<!-- simplified representation of the template -->
<abp-table
[value]="(book$ | async)?.items || []"
[totalRecords]="(book$ | async)?.totalCount"
<ngx-datatable
[rows]="(book$ | async)?.items || []"
[count]="(book$ | async)?.totalCount || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE -->
```
@ -113,7 +95,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
...or...
```ts
```js
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
@ -128,32 +110,41 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
```html
<!-- simplified representation of the template -->
<abp-table
[value]="books$ | async"
[totalRecords]="bookCount$ | async"
<ngx-datatable
[rows]="(books$ | async) || []"
[count]="(bookCount$ | async) || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
```
> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Refresh Table on Create/Update/Delete
`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again.
```ts
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get);
```js
this.bookService.createByInput(form.value)
.subscribe(() => {
this.list.get();
// Other subscription logic here
});
```
...or...
```ts
this.bookService.createByInput(form.value)
.subscribe(() => {
this.list.get();
// Other subscription logic here
})
```js
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get);
```
> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Implement Server-Side Search in a Table
`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding.
@ -163,3 +154,26 @@ this.bookService.createByInput(form.value)
<input type="text" name="search" [(ngModel)]="list.filter">
```
## Breaking Change with ABP v3.0
We had to modify the `ListService` to make it work with `ngx-datatable`. Previously, the minimum value for `page` property was `1` and you could use it like this:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[(page)]="list.page"
></abp-table>
```
As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for inital page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[page]="list.page + 1"
(pageChange)="list.page = $event - 1"
></abp-table>
```
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.

370
docs/en/UI/Angular/Migration-Guide-v3.md

@ -0,0 +1,370 @@
# v2.9 to v3.0 Angular UI Migration Guide
## What Changed in v3.0?
### Angular 10
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though.
#### What to Do When Migrating?
Open a terminal at your root folder and run the following command:
```sh
yarn ng update @angular/cli @angular/core --force
```
This will make the following modifications:
- Update your package.json and install new packages
- Revise tsconfig.json files to create a "Solution Style" configuration
- Rename `browserlist` as `.browserlistrc`
On the other hand, it would be better if you check which packages to update first with `yarn ng update` command alone. Angular will give you a list of packages to update.
![Table of packages to update](./images/table-of-packages-to-update.png)
When Angular reports the packages above, your command would look like this:
```sh
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
```
> If Angular complains about uncommited changes in your repo, you can either commit/stash them or add `--allow-dirty` parameter to the command.
### Config Modules
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows:
```ts
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```ts
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
```
Although working, this had a few disadvantages:
- Every module came in two independent packages, but in reality, those packages were interdependent.
- Configuring lazy loaded modules required a wrapper module.
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size.
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this:
```ts
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```ts
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
```
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps.
#### A Better Example
AppModule:
```ts
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
AppRoutingModule:
```ts
import { DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module')
.then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity')
.then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () => import('@abp/ng.tenant-management')
.then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () => import('@abp/ng.setting-management')
.then(m => m.SettingManagementModule.forLazy()),
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
> You may have noticed that we used `DynamicLayoutComponent` at top level route component. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your app routing.
#### What to Do When Migrating?
- Remove config packages from your project using `yarn remove`.
- Import config modules from secondary entry points (e.g. `@abp/ng.identity/config`).
- Call static `forRoot` method of all new config modules, even if a configuration is not passed.
- Call static `forRoot` method of `ThemeBasicModule` (or `ThemeLeptonModule` if commercial) and remove `SharedModule` from imports (unless you have added anything that is necessary for your root module in it).
- Import lazy ABP modules directly in app routing module (e.g. `() => import('@abp/ng.identity').then(...)`).
- Call static `forLazy` method of all lazy modules inside `then`, even if a configuration is not passed.
- [OPTIONAL] Add an empty parent route with `DynamicLayoutComponent` for better performance and UX.
### RoutesService
In ABP v2.x, adding a route to the menu was done by one of two ways:
- [Via `routes` Property in `AppRoutingModule`](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule)
- [Via ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate)
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in `ConfigState` (breaking change). Instead, there is a new service called `RoutesService` which is used for adding, patching, or removing menu items. Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Check if you have ever used `ConfigState` or `ConfigStateService` to add any routes. If so, replace them with `add` method of `RoutesService`.
- Check if you have ever patched a route. If so, convert them to `patch` method of `RoutesService`.
- Double-check that you are using absolute paths and providing a `parentName` instead of `children` property for sub-menu items in `add` or `patch` method calls.
### NavItemsService
In ABP v2.x, adding a navigation element was done [via LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Replace all `dispatchAddNavigationElement` calls with `addItems` method of `NavItemsService`.
### ngx-datatable
Until v3, we had been using a custom component, `abp-table`, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues.
As of ABP v3, we have switched to a battle-tested, well-executed data grid: [ngx-datatable](https://github.com/swimlane/ngx-datatable). All ABP modules will come with ngx-datatable already implemented in them. `ThemeSharedModule` already exports `NgxDatatableModule`. So, if you install the package by running `yarn add @swimlane/ngx-datatable` in your terminal, it will be available for use in all modules of your app.
For proper styling, you need to add the following in the styles section of your angular.json file:
```json
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
}
```
Since `abp-table` is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the [ListService](./List-Service.md) to work well with ngx-datatable and [introduced](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) two new directives: `NgxDatatableListDirective` and `NgxDatatableDefaultDirective`.
The usage of those directives is rather simple:
```ts
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
```
...and in component template...
```html
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
```
Once you bind the injected `ListService` instance through `NgxDatatableListDirective`, you no longer need to worry about pagination or sorting. Similarly, `NgxDatatableDefaultDirective` gets rid of several property bindings to make ngx-datatable fit our styles.
#### A Better Example
```html
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
```
#### What to Do When Migrating?
- Install `@swimlane/ngx-datatable` package.
- Add ngx-datatable styles in the angular.json file.
- If you can, update your modules according to the example above.
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the [breaking change described here](./List-Service.md).
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.
### Deprecated Interfaces
Some interfaces have long been marked as deprecated and now they are removed.
#### What to Do When Migrating?
- Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281)
## What's Next?
* [Service Proxies](./Service-Proxies.md)

216
docs/en/UI/Angular/Modifying-the-Menu.md

@ -25,9 +25,99 @@ export const environment = {
## How to Add a Navigation Element
### Via `RoutesService`
You can add routes to the menu by calling the `add` method of `RoutesService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
]);
}
}
```
An alternative and probably cleaner way is to use a route provider. First create a provider:
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
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,
},
]);
};
}
```
...and then in app.module.ts...
```js
import { NgModule } from '@angular/core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
// imports, declarations, and bootstrap
})
export class AppModule {}
```
Here is what every property works as:
- `path` is the absolute path of the navigation element.
- `name` is the label of the navigation element. A localization key or a localization object can be passed.
- `parentName` is a reference to the `name` of the parent route in the menu and is used for creating multi-level menu items.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `order` is the order of the navigation element. "Administration" has an order of `100`, so keep that in mind when ordering top level menu items.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `layout` defines in which layout the route will be loaded. (default: `eLayoutType.empty`)
- `invisible` makes the item invisible in the menu. (default: `false`)
### Via `routes` Property in `AppRoutingModule`
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 `ConfigState`. `ApplicationLayoutComponent` gets routes from store and displays them on the menu.
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 add the `routes` property like below:
@ -37,7 +127,7 @@ You can add the `routes` property like below:
data: {
routes: {
name: 'Your navigation',
order: 3,
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
children: [
@ -48,99 +138,77 @@ You can add the `routes` property like below:
requiredPolicy: 'permission key here',
},
],
} as ABP.Route, // can be imported from @abp/ng.core
}
},
},
}
```
- `name` is the label of the navigation element. A localization key or a localization object can be passed.
- `order` is the order of the navigation element.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property.
After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## Via ConfigState
The `dispatchAddRoute` method of `ConfigStateService` adds a new navigation element to the menu.
Alternatively, you can do this:
```js
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 2,
requiredPolicy: 'MyProjectName.MyNewPage',
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`.
If you want **to add a child route, you can do this:**
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRoute: ABP.Route = {
parentName: eIdentityRouteNames.IdentityManagement,
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 3,
requiredPolicy: 'MyProjectName.MyNewPage'
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
{
path: 'your-path',
data: {
routes: [
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
] as ABP.Route[], // can be imported from @abp/ng.core
},
}
```
The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`.
The advantage of the second method is that you are not bound to the parent/child structure and use any paths you like.
The new route will be added like below:
After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png)
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## How to Patch a Navigation Element
## How to Patch or Remove a Navigation Element
The `dispatchPatchRouteByName` method finds a route by its name and replaces its configuration in the store with the new configuration passed as the second parameter.
The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children.
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
// this.routes is instance of RoutesService
// eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
const dashboardRouteConfig: ABP.Route = {
path: '/dashboard',
name: '::Menu:Dashboard',
parentName: '::Menu:Home',
order: 1,
layout: eLayoutType.application,
};
const newRouteConfig: Partial<ABP.Route> = {
const newHomeRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration,
parentName: eThemeSharedRouteNames.Administration,
order: 0,
children: [
{
name: 'Dashboard',
path: 'dashboard',
},
],
};
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig);
// returns a state stream which emits after dispatch action is complete
this.routes.add([dashboardRouteConfig]);
this.routes.patch('::Menu:Home', newHomeRouteConfig);
this.routes.remove(['Your navigation']);
```
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
* Added an icon.
* Specified the order.
* Added a child route named _Dashboard_.
- Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
- Added an icon to _Home_.
- Specified the order and made _Home_ the first item in list.
- Added a route named _Dashboard_ as a child of _Home_.
- Removed _Your navigation_ along with its child route.
After the patch above, navigation elements looks like below:
After the operations above, the new menu looks like below:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)

BIN
docs/en/UI/Angular/images/table-of-packages-to-update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

2
docs/en/Unit-Of-Work.md

@ -325,7 +325,7 @@ public async Task<int> CreateAsync(string name)
If your intent is just to save the changes after creating/updating/deleting an entity, it is suggested to use the `autoSave` option instead of manually using the `CurrentUnitOfWork.SaveChangesAsync()`.
> **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` unless you really need it.
> **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` and don't set `autoSave` to `true` unless you really need it.
>
> **Note-2**: If you use `Guid` as the primary key, you never need to save changes on insert to just get the generated id, because `Guid` keys are set in the application and are immediately available once you create a new entity.

180
docs/en/Virtual-File-System.md

@ -1,40 +1,30 @@
## Virtual File System
# Virtual File System
The Virtual File System makes it possible to manage files that do not physically exist on the file system (disk). It's mainly used to embed (js, css, image..) files into assemblies and use them like physical files at runtime.
### Volo.Abp.VirtualFileSystem Package
## Installation
Volo.Abp.VirtualFileSystem is the core package of the virtual file system. Install it in your project using the package manager console (PMC):
> Most of the times you don't need to manually install this package since it comes pre-installed with the [application startup template](Startup-Templates/Application.md).
```
Install-Package Volo.Abp.VirtualFileSystem
```
[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) is the main page of the Virtual File System.
> This package is already installed by default with the startup template. So, most of the time, you will not need to install it manually.
Use the ABP CLI to add this package to your project:
Then you can add **AbpVirtualFileSystemModule** dependency to your module:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.VirtualFileSystem` package.
* Run `abp add-package Volo.Abp.VirtualFileSystem` command.
```c#
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
If you want to do it manually, install the [Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpVirtualFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
//...
}
}
```
## Working with the Embedded Files
#### Registering Embedded Files
### Embed the Files
A file should be first marked as an embedded resource to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example:
A file should be first marked as an **embedded resource** to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example:
![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png)
If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your **.csproj** file:
If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your `.csproj` file:
````C#
<ItemGroup>
@ -45,76 +35,60 @@ If you want to add multiple files, this can be tedious. Alternatively, you can d
This configuration recursively adds all files under the **MyResources** folder of the project (including the files you will add in the future).
Then the module needs to be configured using `AbpVirtualFileSystemOptions` to register the embedded files to the virtual file system. Example:
Embedding a file in the project/assembly may cause problems if a file name contains some special chars. To overcome this limitation;
````C#
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
1. Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project that contains the embedded resource(s).
2. Add `<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>` into the `<PropertyConfig>...</PropertyConfig>` section of your `.csproj` file.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//Register all embedded files of this assembly to the virtual file system
options.FileSets.AddEmbedded<MyModule>("YourRootNameSpace");
});
> While these two steps are optional and ABP can work without these configuration, it is strongly suggested to make it.
//...
}
}
}
````
### Configure the AbpVirtualFileSystemOptions
The `AddEmbedded` extension method takes a class, finds all embedded files from the assembly of the given class and registers them to the virtual file system. More concisely it could be written as follows:
Use `AbpVirtualFileSystemOptions` [options class](Options.md) to register the embedded files to the virtual file system in the `ConfigureServices` method of your [module](Module-Development-Basics.md).
````C#
options.FileSets.Add(
new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace");
**Example: Add embedded files to the virtual file system**
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
````
> "YourRootNameSpace" is the root namespace of your project. It can be empty if your root namespace is empty.
The `AddEmbedded` extension method takes a class, finds all embedded files from the **assembly of the given class** and registers them to the virtual file system. It is common to pass the module class as the generic argument.
#### Getting Virtual Files: IVirtualFileProvider
`AddEmbedded` can get two optional parameters;
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents:
* `baseNamespace`: This may only needed if you didn't configure the `GenerateEmbeddedFilesManifest` step explained above and your root namespace is not empty. In this case, set your root namespace here.
* `baseFolder`: If you don't want to expose all embedded files in the project, but only want to expose a specific folder (and sub folders/files), then you can set the base folder relative to your project root page.
````C#
public class MyService
**Example: Add files under the `MyFiles` folder in the project**
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
private readonly IVirtualFileProvider _virtualFileProvider;
options.FileSets.AddEmbedded<MyModule>(
baseNamespace: "Acme.BookStore.MyFiles",
baseFolder: "/MyFiles"
);
});
````
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
This example assumes;
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP
* Your project root (default) namespace is `Acme.BookStore`.
* Your project has a folder, named `MyFiles`
* You only want to add `MyFiles` folder to the virtual file system.
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js");
}
}
````
### Dealing With Embedded Files During Development
#### Dealing With Embedded Files During Development
Embedding a file into a module assembly and being able to use it from another project just by referencing the assembly (or adding a nuget package) is invaluable for creating a re-usable module. However, it does make it a little bit harder to develop the module itself.
Embedding a file into an assembly and being able to use it from another project just by referencing the assembly (or adding a NuGet package) is invaluable for creating a re-usable module. However, it makes it a little bit harder to develop the module itself.
Let's assume that you're developing a module that contains an embedded JavaScript file. Whenever you change this file you must re-compile the project, re-start the application and refresh the browser page to take the change. Obviously, this is very time consuming and tedious.
What is needed is the ability for the application to directly use the physical file at development time and a have a browser refresh reflect any change made in the JavaScript file. The `ReplaceEmbeddedByPhysical` method makes all this possible.
The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time.
The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time.
````C#
[DependsOn(typeof(MyModule))]
@ -128,29 +102,63 @@ public class MyWebAppModule : AbpModule
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//ReplaceEmbeddedByPhysical gets the root folder of the MyModule project
options.FileSets.ReplaceEmbeddedByPhysical<MyModule>(
Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}MyModuleProject", Path.DirectorySeparatorChar))
Path.Combine(
hostingEnvironment.ContentRootPath,
string.Format(
"..{0}MyModuleProject",
Path.DirectorySeparatorChar
)
)
);
});
}
//...
}
}
````
The code above assumes that `MyWebAppModule` and `MyModule` are two different projects in a Visual Studio solution and `MyWebAppModule` depends on the `MyModule`.
### ASP.NET Core Integration
> The [application startup template](Startup-Templates/Application.md) already uses this technique for the localization files. So, when you change a localization file it automatically detects the change.
## IVirtualFileProvider
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents:
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider
.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString();
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider
.GetDirectoryContents("/MyResources/js");
}
}
````
## ASP.NET Core Integration
The Virtual File System is well integrated to ASP.NET Core:
* Virtual files can be used just like physical (static) files in a web application.
* Js, css, image files and all other web content types can be embedded into assemblies and used just like the physical files.
* An application (or another module) can override a virtual file of a module just like placing a file with the same name and extension into the same folder of the virtual file.
* An application (or another module) can **override a virtual file** of a module just like placing a file with the same name and extension into the same folder of the virtual file.
#### Virtual Files Middleware
### UseVirtualFiles Middleware
The Virtual Files Middleware is used to serve embedded (js, css, image...) files to clients/browsers just like physical files in the **wwwroot** folder. Add it just after the static file middleware as shown below:
@ -160,4 +168,14 @@ app.UseVirtualFiles();
Adding virtual files middleware after the static files middleware makes it possible to override a virtual file with a real physical file simply by placing it in the same location as the virtual file.
>The Virtual File Middleware only serves the virtual wwwroot folder contents - just like the other static files.
> `UseVirtualFiles()` is already configured for the [application startup template](Startup-Templates/Application.md).
#### Static Virtual File Folders
By default, ASP.NET Core only allows the `wwwroot` folder to contain the static files consumed by the clients. When you use the `UseVirtualFiles` middleware, the following folders also can contain static files:
* Pages
* Views
* Themes
This allows to add `.js`, `.css`... files near to your `.cshtml` file that is easier to develop and maintain your project.

4
docs/en/docs-nav.json

@ -366,6 +366,10 @@
{
"text": "Angular",
"items": [
{
"text": "Migration Guide v2.x to v3",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{
"text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md"

21
framework/Volo.Abp.sln

@ -305,6 +305,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure.Tests", "test\Volo.Abp.BlobStoring.Azure.Tests\Volo.Abp.BlobStoring.Azure.Tests.csproj", "{A80E9A0B-8932-4B5D-83FB-6751708FD484}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Oracle", "src\Volo.Abp.EntityFrameworkCore.Oracle\Volo.Abp.EntityFrameworkCore.Oracle.csproj", "{1738845A-5348-4EB8-B736-CD1D22A808B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis", "src\Volo.Abp.Caching.StackExchangeRedis\Volo.Abp.Caching.StackExchangeRedis.csproj", "{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis.Tests", "test\Volo.Abp.Caching.StackExchangeRedis.Tests\Volo.Abp.Caching.StackExchangeRedis.Tests.csproj", "{60D0E384-965E-4F81-9D71-B28F419254FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -907,6 +913,18 @@ Global
{A80E9A0B-8932-4B5D-83FB-6751708FD484}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A80E9A0B-8932-4B5D-83FB-6751708FD484}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A80E9A0B-8932-4B5D-83FB-6751708FD484}.Release|Any CPU.Build.0 = Release|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.Build.0 = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.Build.0 = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1061,6 +1079,9 @@ Global
{75E5C841-5F36-4C44-A532-57CB8E7FFE15} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{C44242F7-D55D-4867-AAF4-A786E404312E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{A80E9A0B-8932-4B5D-83FB-6751708FD484} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{1738845A-5348-4EB8-B736-CD1D22A808B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{60D0E384-965E-4F81-9D71-B28F419254FC} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

2
framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.5" />
</ItemGroup>
</Project>

27
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53450/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Authentication.OAuth": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53451/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53760/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.MultiTenancy": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53763/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52302/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.Client": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:52303/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs

@ -14,5 +14,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public string UserName { get; set; }
public string Email { get; set; }
public string[] Roles { get; set; }
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61183/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Bootstrap": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53762/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Bundling": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53765/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:56918/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.TenantSwitch": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:56919/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53766/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Packages": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53768/"
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyScriptContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
{
public class UppyScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/uppy/uppy.min.js");
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Uppy/UppyStyleContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
{
public class UppyStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/uppy/uppy.min.css");
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50398/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:50399/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj

@ -22,8 +22,6 @@
<ItemGroup>
<Content Remove="wwwroot\**\*.*" />
<Content Remove="Properties\launchSettings.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj

@ -18,8 +18,6 @@
<ItemGroup>
<Content Remove="compilerconfig.json" />
<Content Remove="Properties\launchSettings.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageSearchBox
{

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml

@ -1,13 +1,17 @@
@using Localization.Resources.AbpUi
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<AbpUiResource> L
<form method="post" class="page-search-form">
<div class="input-group">
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value">
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value" />
<div class="input-group-append">
<abp-button button-type="Primary" type="submit">
<i class="fa fa-search" aria-hidden="true" style="margin-left: 10%; margin-top:50%;" ></i>
<i class="fa fa-search" aria-hidden="true"></i>
</abp-button>
</div>
</div>
</form>
</form>

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml

@ -0,0 +1,4 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51544/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:51545/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj

@ -23,9 +23,7 @@
<ItemGroup>
<Content Remove="wwwroot\**\*.*" />
<Content Remove="compilerconfig.json" />
<Content Remove="Properties\launchSettings.json" />
<None Include="compilerconfig.json" />
<None Include="Properties\launchSettings.json" />
</ItemGroup>
<ItemGroup>

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53761/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI.Widgets": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53764/"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55913/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.UI": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo.Abp.AspNetCore.Mvc.UI.csproj

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUglify" Version="1.5.13" />
<PackageReference Include="NUglify" Version="1.6.3" />
</ItemGroup>
<ItemGroup>

27
framework/src/Volo.Abp.AspNetCore.Mvc/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53767/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53769/"
}
}
}

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj

@ -26,9 +26,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.5" />
</ItemGroup>
</Project>

5
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs

@ -16,6 +16,7 @@ using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
@ -25,6 +26,7 @@ using Microsoft.Extensions.Localization;
using Volo.Abp.ApiVersioning;
using Volo.Abp.AspNetCore.Mvc.ApiExploring;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.DataAnnotations;
using Volo.Abp.AspNetCore.Mvc.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Json;
using Volo.Abp.AspNetCore.Mvc.Localization;
@ -164,6 +166,9 @@ namespace Volo.Abp.AspNetCore.Mvc
partManager.FeatureProviders.Add(new AbpConventionalControllerFeatureProvider(application));
partManager.ApplicationParts.AddIfNotContains(typeof(AbpAspNetCoreMvcModule).Assembly);
context.Services.Replace(ServiceDescriptor.Singleton<IValidationAttributeAdapterProvider, AbpValidationAttributeAdapterProvider>());
context.Services.AddSingleton<ValidationAttributeAdapterProvider>();
Configure<MvcOptions>(mvcOptions =>
{
mvcOptions.AddAbp(context.Services);

1
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs

@ -5,7 +5,6 @@ using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features;
using Volo.Abp.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata;
using Volo.Abp.AspNetCore.Mvc.Response;
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation;

3
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs

@ -117,7 +117,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
Id = _currentUser.Id,
TenantId = _currentUser.TenantId,
UserName = _currentUser.UserName,
Email = _currentUser.Email
Email = _currentUser.Email,
Roles = _currentUser.Roles
};
}

15
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Authentication/ChallengeAccountController.cs

@ -52,6 +52,21 @@ namespace Volo.Abp.AspNetCore.Mvc.Authentication
return new SignOutResult(ChallengeAuthenticationSchemas);
}
[HttpGet]
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid").Value ?? string.Empty;
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await Logout();
}
}
return NoContent();
}
protected RedirectResult RedirectSafely(string returnUrl, string returnUrlHash = null)
{
return Redirect(GetRedirectUrl(returnUrl, returnUrlHash));

34
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/AbpValidationAttributeAdapterProvider.cs

@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class AbpValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly ValidationAttributeAdapterProvider _defaultAdapter;
public AbpValidationAttributeAdapterProvider(ValidationAttributeAdapterProvider defaultAdapter)
{
_defaultAdapter = defaultAdapter;
}
public virtual IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
var type = attribute.GetType();
if (type == typeof(DynamicStringLengthAttribute))
{
return new DynamicStringLengthAttributeAdapter((DynamicStringLengthAttribute) attribute, stringLocalizer);
}
if (type == typeof(DynamicMaxLengthAttribute))
{
return new DynamicMaxLengthAttributeAdapter((DynamicMaxLengthAttribute) attribute, stringLocalizer);
}
return _defaultAdapter.GetAttributeAdapter(attribute, stringLocalizer);
}
}
}

46
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicMaxLengthAttributeAdapter.cs

@ -0,0 +1,46 @@
using System.Globalization;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class DynamicMaxLengthAttributeAdapter : AttributeAdapterBase<DynamicMaxLengthAttribute>
{
private readonly string _max;
public DynamicMaxLengthAttributeAdapter(
DynamicMaxLengthAttribute attribute,
IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_max = Attribute.Length.ToString(CultureInfo.InvariantCulture);
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
Check.NotNull(validationContext, nameof(validationContext));
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Length
);
}
public override void AddValidation(ClientModelValidationContext context)
{
Check.NotNull(context, nameof(context));
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max);
if (Attribute.Length != int.MaxValue)
{
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max);
}
}
}
}

54
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/DataAnnotations/DynamicStringLengthAttributeAdapter.cs

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Volo.Abp.Validation;
namespace Volo.Abp.AspNetCore.Mvc.DataAnnotations
{
public class DynamicStringLengthAttributeAdapter : AttributeAdapterBase<DynamicStringLengthAttribute>
{
private readonly string _max;
private readonly string _min;
public DynamicStringLengthAttributeAdapter(
DynamicStringLengthAttribute attribute,
IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture);
_min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture);
}
public override void AddValidation(ClientModelValidationContext context)
{
Check.NotNull(context, nameof(context));
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context));
if (Attribute.MaximumLength != int.MaxValue)
{
MergeAttribute(context.Attributes, "data-val-length-max", _max);
}
if (Attribute.MinimumLength != 0)
{
MergeAttribute(context.Attributes, "data-val-length-min", _min);
}
}
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
Check.NotNull(validationContext, nameof(validationContext));
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.MaximumLength,
Attribute.MinimumLength
);
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61851/",
"sslPort": 44301
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Serilog": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj

@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.8.0" />
<PackageReference Include="Serilog" Version="2.9.0" />
</ItemGroup>
</Project>

27
framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53900/",
"sslPort": 44362
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.SignalR": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53783/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.TestBase": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53784/"
}
}
}

2
framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj

@ -24,7 +24,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.5" />
</ItemGroup>
</Project>

27
framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53374/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53375/"
}
}
}

2
framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
</ItemGroup>
<ItemGroup>

6
framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj

@ -15,9 +15,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.5.0" />
<PackageReference Include="Autofac" Version="4.9.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.2" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="5.0.0" />
<PackageReference Include="Autofac" Version="5.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
</ItemGroup>
<ItemGroup>

6
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs

@ -1,6 +1,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Volo.Abp.Modularity;
using Volo.Abp.Quartz;
@ -18,6 +19,11 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
context.Services.AddConventionalRegistrar(new AbpQuartzConventionalRegistrar());
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton(typeof(QuartzPeriodicBackgroundWorkerAdapter<>));
}
public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
{
var options = context.ServiceProvider.GetService<IOptions<AbpBackgroundWorkerOptions>>().Value;

7
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorkerAdapter.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.BackgroundWorkers.Quartz
{
public interface IQuartzBackgroundWorkerAdapter : IQuartzBackgroundWorker
{
void BuildWorker(IBackgroundWorker worker);
}
}

88
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs

@ -0,0 +1,88 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz
{
public class QuartzPeriodicBackgroundWorkerAdapter<TWorker> : QuartzBackgroundWorkerBase,
IQuartzBackgroundWorkerAdapter
where TWorker : IBackgroundWorker
{
private readonly MethodInfo _doWorkAsyncMethod;
private readonly MethodInfo _doWorkMethod;
public QuartzPeriodicBackgroundWorkerAdapter()
{
AutoRegister = false;
_doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic);
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic);
}
public void BuildWorker(IBackgroundWorker worker)
{
int? period;
var workerType = worker.GetType();
if (worker is AsyncPeriodicBackgroundWorkerBase || worker is PeriodicBackgroundWorkerBase)
{
if (typeof(TWorker) != worker.GetType())
{
throw new ArgumentException($"{nameof(worker)} type is different from the generic type");
}
var timer = (AbpTimer) worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
period = timer?.Period;
}
else
{
return;
}
if (period == null)
{
return;
}
JobDetail = JobBuilder
.Create<QuartzPeriodicBackgroundWorkerAdapter<TWorker>>()
.WithIdentity(workerType.FullName)
.Build();
Trigger = TriggerBuilder.Create()
.WithIdentity(workerType.FullName)
.WithSimpleSchedule(builder => builder.WithInterval(TimeSpan.FromMilliseconds(period.Value)).RepeatForever())
.Build();
}
public override async Task Execute(IJobExecutionContext context)
{
var worker = (IBackgroundWorker) ServiceProvider.GetService(typeof(TWorker));
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider);
switch (worker)
{
case AsyncPeriodicBackgroundWorkerBase asyncWorker:
{
if (_doWorkAsyncMethod != null)
{
await (Task) _doWorkAsyncMethod.Invoke(asyncWorker, new object[] {workerContext});
}
break;
}
case PeriodicBackgroundWorkerBase syncWorker:
{
if (_doWorkMethod != null)
{
_doWorkMethod.Invoke(syncWorker, new object[] {workerContext});
}
break;
}
}
}
}
}

18
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs

@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.DependencyInjection;
@ -50,6 +51,19 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
await DefaultScheduleJobAsync(quartzWork);
}
}
else
{
var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
var workerAdapter = Activator.CreateInstance(adapterType) as IQuartzBackgroundWorkerAdapter;
workerAdapter?.BuildWorker(worker);
if (workerAdapter?.Trigger != null)
{
await DefaultScheduleJobAsync(workerAdapter);
}
}
}
protected virtual async Task DefaultScheduleJobAsync(IQuartzBackgroundWorker quartzWork)
@ -66,4 +80,4 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
}
}
}
}
}

2
framework/src/Volo.Abp.BlobStoring.Azure/Volo.Abp.BlobStoring.Azure.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.3" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.4" />
</ItemGroup>
</Project>

1
framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobContainerConfigurationExtensions.cs

@ -15,6 +15,7 @@ namespace Volo.Abp.BlobStoring.Azure
Action<AzureBlobProviderConfiguration> azureConfigureAction)
{
containerConfiguration.ProviderType = typeof(AzureBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<AzureBlobNamingNormalizer>();
azureConfigureAction(new AzureBlobProviderConfiguration(containerConfiguration));

52
framework/src/Volo.Abp.BlobStoring.Azure/Volo/Abp/BlobStoring/Azure/AzureBlobNamingNormalizer.cs

@ -0,0 +1,52 @@
using System.Text.RegularExpressions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BlobStoring.Azure
{
public class AzureBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
/// </summary>
public virtual string NormalizeContainerName(string containerName)
{
// All letters in a container name must be lowercase.
containerName = containerName.ToLower();
// Container names can contain only letters, numbers, and the dash (-) character.
containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty);
// Every dash (-) character must be immediately preceded and followed by a letter or number;
// consecutive dashes are not permitted in container names.
// Container names must start or end with a letter or number
containerName = Regex.Replace(containerName, "-{2,}", "-");
containerName = Regex.Replace(containerName, "^-", string.Empty);
containerName = Regex.Replace(containerName, "-$", string.Empty);
// Container names must be from 3 through 63 characters long.
if (containerName.Length < 3)
{
var length = containerName.Length;
for (var i = 0; i < 3 - length; i++)
{
containerName += "0";
}
}
if (containerName.Length > 63)
{
containerName = containerName.Substring(0, 63);
}
return containerName;
}
/// <summary>
///https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#blob-names
/// </summary>
public virtual string NormalizeBlobName(string blobName)
{
return blobName;
}
}
}

2
framework/src/Volo.Abp.BlobStoring.FileSystem/Volo.Abp.BlobStoring.FileSystem.csproj

@ -16,7 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" />
<PackageReference Include="Polly" Version="7.2.0" />
<PackageReference Include="Polly" Version="7.2.1" />
</ItemGroup>
</Project>

9
framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobContainerConfigurationExtensions.cs

@ -9,16 +9,17 @@ namespace Volo.Abp.BlobStoring.FileSystem
{
return new FileSystemBlobProviderConfiguration(containerConfiguration);
}
public static BlobContainerConfiguration UseFileSystem(
this BlobContainerConfiguration containerConfiguration,
Action<FileSystemBlobProviderConfiguration> fileSystemConfigureAction)
{
containerConfiguration.ProviderType = typeof(FileSystemBlobProvider);
containerConfiguration.NamingNormalizers.TryAdd<FileSystemBlobNamingNormalizer>();
fileSystemConfigureAction(new FileSystemBlobProviderConfiguration(containerConfiguration));
return containerConfiguration;
}
}
}
}

41
framework/src/Volo.Abp.BlobStoring.FileSystem/Volo/Abp/BlobStoring/FileSystem/FileSystemBlobNamingNormalizer.cs

@ -0,0 +1,41 @@
using System;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BlobStoring.FileSystem
{
public class FileSystemBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency
{
private readonly IOSPlatformProvider _iosPlatformProvider;
public FileSystemBlobNamingNormalizer(IOSPlatformProvider iosPlatformProvider)
{
_iosPlatformProvider = iosPlatformProvider;
}
public virtual string NormalizeContainerName(string containerName)
{
return Normalize(containerName);
}
public virtual string NormalizeBlobName(string blobName)
{
return Normalize(blobName);
}
protected virtual string Normalize(string fileName)
{
var os = _iosPlatformProvider.GetCurrentOSPlatform();
if (os == OSPlatform.Windows)
{
// A filename cannot contain any of the following characters: \ / : * ? " < > |
// In order to support the directory included in the blob name, remove / and \
fileName = Regex.Replace(fileName, "[:\\*\\?\"<>\\|]", string.Empty);
}
return fileName;
}
}
}

62
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainer.cs

@ -1,7 +1,9 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
@ -84,18 +86,22 @@ namespace Volo.Abp.BlobStoring
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
public BlobContainer(
string containerName,
BlobContainerConfiguration configuration,
IBlobProvider provider,
ICurrentTenant currentTenant,
ICancellationTokenProvider cancellationTokenProvider)
ICancellationTokenProvider cancellationTokenProvider,
IServiceProvider serviceProvider)
{
ContainerName = containerName;
Configuration = configuration;
Provider = provider;
CurrentTenant = currentTenant;
CancellationTokenProvider = cancellationTokenProvider;
ServiceProvider = serviceProvider;
}
public virtual async Task SaveAsync(
@ -106,11 +112,13 @@ namespace Volo.Abp.BlobStoring
{
using (CurrentTenant.Change(GetTenantIdOrNull()))
{
var (normalizedContainerName, normalizedBlobName) = NormalizeNaming(ContainerName, name);
await Provider.SaveAsync(
new BlobProviderSaveArgs(
ContainerName,
normalizedContainerName,
Configuration,
name,
normalizedBlobName,
stream,
overrideExisting,
CancellationTokenProvider.FallbackToProvider(cancellationToken)
@ -125,11 +133,14 @@ namespace Volo.Abp.BlobStoring
{
using (CurrentTenant.Change(GetTenantIdOrNull()))
{
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.DeleteAsync(
new BlobProviderDeleteArgs(
ContainerName,
normalizedContainerName,
Configuration,
name,
normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken)
)
);
@ -142,11 +153,14 @@ namespace Volo.Abp.BlobStoring
{
using (CurrentTenant.Change(GetTenantIdOrNull()))
{
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.ExistsAsync(
new BlobProviderExistsArgs(
ContainerName,
normalizedContainerName,
Configuration,
name,
normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken)
)
);
@ -158,7 +172,7 @@ namespace Volo.Abp.BlobStoring
CancellationToken cancellationToken = default)
{
var stream = await GetOrNullAsync(name, cancellationToken);
if (stream == null)
{
//TODO: Consider to throw some type of "not found" exception and handle on the HTTP status side
@ -175,11 +189,14 @@ namespace Volo.Abp.BlobStoring
{
using (CurrentTenant.Change(GetTenantIdOrNull()))
{
var (normalizedContainerName, normalizedBlobName) =
NormalizeNaming(ContainerName, name);
return await Provider.GetOrNullAsync(
new BlobProviderGetArgs(
ContainerName,
normalizedContainerName,
Configuration,
name,
normalizedBlobName,
CancellationTokenProvider.FallbackToProvider(cancellationToken)
)
);
@ -195,5 +212,28 @@ namespace Volo.Abp.BlobStoring
return CurrentTenant.Id;
}
protected virtual (string, string) NormalizeNaming(string containerName, string blobName)
{
if (!Configuration.NamingNormalizers.Any())
{
return (containerName, blobName);
}
using (var scope = ServiceProvider.CreateScope())
{
foreach (var normalizerType in Configuration.NamingNormalizers)
{
var normalizer = scope.ServiceProvider
.GetRequiredService(normalizerType)
.As<IBlobNamingNormalizer>();
containerName = normalizer.NormalizeContainerName(containerName);
blobName = normalizer.NormalizeBlobName(blobName);
}
return (containerName, blobName);
}
}
}
}
}

10
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs

@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Collections;
namespace Volo.Abp.BlobStoring
{
@ -18,17 +19,20 @@ namespace Volo.Abp.BlobStoring
/// then the container is shared by all tenants in the system.
///
/// This can be <code>true</code> even if your application is not multi-tenant.
///
///
/// Default: true.
/// </summary>
public bool IsMultiTenant { get; set; } = true;
public ITypeList<IBlobNamingNormalizer> NamingNormalizers { get; }
[NotNull] private readonly Dictionary<string, object> _properties;
[CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration;
public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null)
{
NamingNormalizers = new TypeList<IBlobNamingNormalizer>();
_fallbackConfiguration = fallbackConfiguration;
_properties = new Dictionary<string, object>();
}
@ -68,4 +72,4 @@ namespace Volo.Abp.BlobStoring
return this;
}
}
}
}

24
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactory.cs

@ -1,4 +1,5 @@
using Volo.Abp.DependencyInjection;
using System;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
@ -7,36 +8,41 @@ namespace Volo.Abp.BlobStoring
public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency
{
protected IBlobProviderSelector ProviderSelector { get; }
protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IServiceProvider ServiceProvider { get; }
public BlobContainerFactory(
IBlobContainerConfigurationProvider configurationProvider,
ICurrentTenant currentTenant,
ICancellationTokenProvider cancellationTokenProvider,
IBlobProviderSelector providerSelector)
ICancellationTokenProvider cancellationTokenProvider,
IBlobProviderSelector providerSelector,
IServiceProvider serviceProvider)
{
ConfigurationProvider = configurationProvider;
CurrentTenant = currentTenant;
CancellationTokenProvider = cancellationTokenProvider;
ProviderSelector = providerSelector;
ServiceProvider = serviceProvider;
}
public virtual IBlobContainer Create(string name)
{
var configuration = ConfigurationProvider.Get(name);
return new BlobContainer(
name,
configuration,
ProviderSelector.Get(name),
CurrentTenant,
CancellationTokenProvider
CancellationTokenProvider,
ServiceProvider
);
}
}
}
}

9
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/IBlobNamingNormalizer.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.BlobStoring
{
public interface IBlobNamingNormalizer
{
string NormalizeContainerName(string containerName);
string NormalizeBlobName(string blobName);
}
}

3
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

25
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.Caching.StackExchangeRedis</AssemblyName>
<PackageId>Volo.Abp.Caching.StackExchangeRedis</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.5" />
</ItemGroup>
</Project>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs

@ -0,0 +1,30 @@
using System;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Modularity;
namespace Volo.Abp.Caching.StackExchangeRedis
{
[DependsOn(
typeof(AbpCachingModule)
)]
public class AbpCachingStackExchangeRedisModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddStackExchangeRedisCache(options =>
{
var redisConfiguration = configuration["Redis:Configuration"];
if (!redisConfiguration.IsNullOrEmpty())
{
options.Configuration = configuration["Redis:Configuration"];
}
});
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, AbpRedisCache>());
}
}
}

294
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs

@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Caching.StackExchangeRedis
{
[DisableConventionalRegistration]
public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems
{
protected static readonly string SetScript;
protected static readonly string AbsoluteExpirationKey;
protected static readonly string SlidingExpirationKey;
protected static readonly string DataKey;
protected static readonly long NotPresent;
private static readonly FieldInfo RedisDatabaseField;
private static readonly MethodInfo ConnectMethod;
private static readonly MethodInfo ConnectAsyncMethod;
private static readonly MethodInfo MapMetadataMethod;
private static readonly MethodInfo GetAbsoluteExpirationMethod;
private static readonly MethodInfo GetExpirationInSecondsMethod;
protected IDatabase RedisDatabase => GetRedisDatabase();
private IDatabase _redisDatabase;
protected string Instance { get; }
static AbpRedisCache()
{
var type = typeof(RedisCache);
RedisDatabaseField = type.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectMethod = type.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic);
ConnectAsyncMethod = type.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic);
MapMetadataMethod = type.GetMethod("MapMetadata", BindingFlags.Instance | BindingFlags.NonPublic);
GetAbsoluteExpirationMethod = type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic);
GetExpirationInSecondsMethod = type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic);
SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString();
NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).To<int>();
}
public AbpRedisCache(IOptions<RedisCacheOptions> optionsAccessor)
: base(optionsAccessor)
{
Instance = optionsAccessor.Value.InstanceName ?? string.Empty;
}
protected virtual void Connect()
{
if (GetRedisDatabase() != null)
{
return;
}
ConnectMethod.Invoke(this, Array.Empty<object>());
}
protected virtual Task ConnectAsync(CancellationToken token = default)
{
if (GetRedisDatabase() != null)
{
return Task.CompletedTask;
}
return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token});
}
public byte[][] GetMany(
IEnumerable<string> keys)
{
keys = Check.NotNull(keys, nameof(keys));
return GetAndRefreshMany(keys, true);
}
public async Task<byte[][]> GetManyAsync(
IEnumerable<string> keys,
CancellationToken token = default)
{
keys = Check.NotNull(keys, nameof(keys));
return await GetAndRefreshManyAsync(keys, true, token);
}
public void SetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options)
{
Connect();
Task.WaitAll(PipelineSetMany(items, options));
}
public async Task SetManyAsync(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options,
CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
await ConnectAsync(token);
await Task.WhenAll(PipelineSetMany(items, options));
}
protected virtual byte[][] GetAndRefreshMany(
IEnumerable<string> keys,
bool getData)
{
Connect();
var keyArray = keys.Select(key => Instance + key).ToArray();
RedisValue[][] results;
if (getData)
{
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = RedisDatabase.HashMemberGetMany(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
Task.WaitAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes));
return bytes;
}
protected virtual async Task<byte[][]> GetAndRefreshManyAsync(
IEnumerable<string> keys,
bool getData,
CancellationToken token = default)
{
token.ThrowIfCancellationRequested();
await ConnectAsync(token);
var keyArray = keys.Select(key => Instance + key).ToArray();
RedisValue[][] results;
if (getData)
{
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey, DataKey);
}
else
{
results = await RedisDatabase.HashMemberGetManyAsync(keyArray, AbsoluteExpirationKey,
SlidingExpirationKey);
}
await Task.WhenAll(PipelineRefreshManyAndOutData(keyArray, results, out var bytes));
return bytes;
}
protected virtual Task[] PipelineRefreshManyAndOutData(
string[] keys,
RedisValue[][] results,
out byte[][] bytes)
{
bytes = new byte[keys.Length][];
var tasks = new Task[keys.Length];
for (var i = 0; i < keys.Length; i++)
{
if (results[i].Length >= 2)
{
MapMetadata(results[i], out DateTimeOffset? absExpr, out TimeSpan? sldExpr);
if (sldExpr.HasValue)
{
TimeSpan? expr;
if (absExpr.HasValue)
{
var relExpr = absExpr.Value - DateTimeOffset.Now;
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
}
else
{
expr = sldExpr;
}
tasks[i] = RedisDatabase.KeyExpireAsync(keys[i], expr);
}
else
{
tasks[i] = Task.CompletedTask;
}
}
if (results[i].Length >= 3 && results[i][2].HasValue)
{
bytes[i] = results[i][2];
}
else
{
bytes[i] = null;
}
}
return tasks;
}
protected virtual Task[] PipelineSetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options)
{
items = Check.NotNull(items, nameof(items));
options = Check.NotNull(options, nameof(options));
var itemArray = items.ToArray();
var tasks = new Task[itemArray.Length];
var creationTime = DateTimeOffset.UtcNow;
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);
for (var i = 0; i < itemArray.Length; i++)
{
tasks[i] = RedisDatabase.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + itemArray[i].Key},
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
itemArray[i].Value
});
}
return tasks;
}
protected virtual void MapMetadata(
RedisValue[] results,
out DateTimeOffset? absoluteExpiration,
out TimeSpan? slidingExpiration)
{
var parameters = new object[] {results, null, null};
MapMetadataMethod.Invoke(this, parameters);
absoluteExpiration = (DateTimeOffset?) parameters[1];
slidingExpiration = (TimeSpan?) parameters[2];
}
protected virtual long? GetExpirationInSeconds(
DateTimeOffset creationTime,
DateTimeOffset? absoluteExpiration,
DistributedCacheEntryOptions options)
{
return (long?) GetExpirationInSecondsMethod.Invoke(null,
new object[] {creationTime, absoluteExpiration, options});
}
protected virtual DateTimeOffset? GetAbsoluteExpiration(
DateTimeOffset creationTime,
DistributedCacheEntryOptions options)
{
return (DateTimeOffset?) GetAbsoluteExpirationMethod.Invoke(null, new object[] {creationTime, options});
}
private IDatabase GetRedisDatabase()
{
if (_redisDatabase == null)
{
_redisDatabase = RedisDatabaseField.GetValue(this) as IDatabase;
}
return _redisDatabase;
}
}
}

47
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisExtensions.cs

@ -0,0 +1,47 @@
using System.Linq;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace Volo.Abp.Caching.StackExchangeRedis
{
public static class AbpRedisExtensions
{
public static RedisValue[][] HashMemberGetMany(
this IDatabase cache,
string[] keys,
params string[] members)
{
var tasks = new Task<RedisValue[]>[keys.Length];
var fields = members.Select(member => (RedisValue) member).ToArray();
var results = new RedisValue[keys.Length][];
for (var i = 0; i < keys.Length; i++)
{
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields);
}
for (var i = 0; i < tasks.Length; i++)
{
results[i] = cache.Wait(tasks[i]);
}
return results;
}
public static async Task<RedisValue[][]> HashMemberGetManyAsync(
this IDatabase cache,
string[] keys,
params string[] members)
{
var tasks = new Task<RedisValue[]>[keys.Length];
var fields = members.Select(member => (RedisValue) member).ToArray();
for (var i = 0; i < keys.Length; i++)
{
tasks[i] = cache.HashGetAsync((RedisKey) keys[i], fields);
}
return await Task.WhenAll(tasks);
}
}
}

2
framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" />
</ItemGroup>
<ItemGroup>

393
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -28,16 +31,16 @@ namespace Volo.Abp.Caching
IDistributedCacheSerializer serializer,
IDistributedCacheKeyNormalizer keyNormalizer,
IHybridServiceScopeFactory serviceScopeFactory) : base(
distributedCacheOption: distributedCacheOption,
cache: cache,
cancellationTokenProvider: cancellationTokenProvider,
serializer: serializer,
keyNormalizer: keyNormalizer,
serviceScopeFactory: serviceScopeFactory)
distributedCacheOption: distributedCacheOption,
cache: cache,
cancellationTokenProvider: cancellationTokenProvider,
serializer: serializer,
keyNormalizer: keyNormalizer,
serviceScopeFactory: serviceScopeFactory)
{
}
}
/// <summary>
/// Represents a distributed cache of <typeparamref name="TCacheItem" /> type.
/// Uses a generic cache key type of <typeparamref name="TCacheKey" /> type.
@ -148,19 +151,151 @@ namespace Volo.Abp.Caching
{
if (hideErrors == true)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
HandleException(ex);
return null;
}
throw;
}
if (cachedBytes == null)
return ToCacheItem(cachedBytes);
}
public virtual KeyValuePair<TCacheKey, TCacheItem>[] GetMany(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null)
{
var keyArray = keys.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
return null;
return GetManyFallback(
keyArray,
hideErrors
);
}
return Serializer.Deserialize<TCacheItem>(cachedBytes);
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
byte[][] cachedBytes;
try
{
cachedBytes = cacheSupportsMultipleItems.GetMany(keyArray.Select(NormalizeKey));
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return ToCacheItemsWithDefaultValues(keyArray);
}
throw;
}
return ToCacheItems(cachedBytes, keyArray);
}
protected virtual KeyValuePair<TCacheKey, TCacheItem>[] GetManyFallback(
TCacheKey[] keys,
bool? hideErrors = null)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
return keys
.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(
key,
Get(key, hideErrors: false)
)
).ToArray();
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return ToCacheItemsWithDefaultValues(keys);
}
throw;
}
}
public virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null,
CancellationToken token = default)
{
var keyArray = keys.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
return await GetManyFallbackAsync(
keyArray,
hideErrors,
token
);
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
byte[][] cachedBytes;
try
{
cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
keyArray.Select(NormalizeKey),
CancellationTokenProvider.FallbackToProvider(token)
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return ToCacheItemsWithDefaultValues(keyArray);
}
throw;
}
return ToCacheItems(cachedBytes, keyArray);
}
protected virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyFallbackAsync(
TCacheKey[] keys,
bool? hideErrors = null,
CancellationToken token = default)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();
foreach (var key in keys)
{
result.Add(new KeyValuePair<TCacheKey, TCacheItem>(
key,
await GetAsync(key, false, token))
);
}
return result.ToArray();
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return ToCacheItemsWithDefaultValues(keys);
}
throw;
}
}
/// <summary>
@ -307,7 +442,7 @@ namespace Volo.Abp.Caching
{
if (hideErrors == true)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
HandleException(ex);
return;
}
@ -354,14 +489,156 @@ namespace Volo.Abp.Caching
}
}
/// <summary>
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
public void SetMany(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null)
{
var itemsArray = items.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
SetManyFallback(
itemsArray,
options,
hideErrors
);
return;
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
cacheSupportsMultipleItems.SetMany(
ToRawCacheItems(itemsArray),
options ?? DefaultCacheOptions
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return;
}
throw;
}
}
protected virtual void SetManyFallback(
KeyValuePair<TCacheKey, TCacheItem>[] items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
foreach (var item in items)
{
Set(
item.Key,
item.Value,
options: options,
hideErrors: false
);
}
}
catch (Exception ex)
{
if (hideErrors == true)
{
HandleException(ex);
return;
}
throw;
}
}
public virtual async Task SetManyAsync(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default)
{
var itemsArray = items.ToArray();
var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
if (cacheSupportsMultipleItems == null)
{
await SetManyFallbackAsync(
itemsArray,
options,
hideErrors,
token
);
return;
}
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
await cacheSupportsMultipleItems.SetManyAsync(
ToRawCacheItems(itemsArray),
options ?? DefaultCacheOptions,
CancellationTokenProvider.FallbackToProvider(token)
);
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return;
}
throw;
}
}
protected virtual async Task SetManyFallbackAsync(
KeyValuePair<TCacheKey, TCacheItem>[] items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
try
{
foreach (var item in items)
{
await SetAsync(
item.Key,
item.Value,
options: options,
hideErrors: false,
token: token
);
}
}
catch (Exception ex)
{
if (hideErrors == true)
{
await HandleExceptionAsync(ex);
return;
}
throw;
}
}
public virtual void Refresh(
TCacheKey key, bool?
hideErrors = null)
hideErrors = null)
{
hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
@ -373,20 +650,14 @@ namespace Volo.Abp.Caching
{
if (hideErrors == true)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
HandleException(ex);
return;
}
throw;
}
}
/// <summary>
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
public virtual async Task RefreshAsync(
TCacheKey key,
bool? hideErrors = null,
@ -410,11 +681,6 @@ namespace Volo.Abp.Caching
}
}
/// <summary>
/// Removes the cache item for given key from cache.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
public virtual void Remove(
TCacheKey key,
bool? hideErrors = null)
@ -429,7 +695,7 @@ namespace Volo.Abp.Caching
{
if (hideErrors == true)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
HandleException(ex);
return;
}
@ -437,13 +703,6 @@ namespace Volo.Abp.Caching
}
}
/// <summary>
/// Removes the cache item for given key from cache.
/// </summary>
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
public virtual async Task RemoveAsync(
TCacheKey key,
bool? hideErrors = null,
@ -467,6 +726,11 @@ namespace Volo.Abp.Caching
}
}
protected virtual void HandleException(Exception ex)
{
AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
}
protected virtual async Task HandleExceptionAsync(Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
@ -478,5 +742,56 @@ namespace Volo.Abp.Caching
.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
}
}
protected virtual KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItems(byte[][] itemBytes, TCacheKey[] itemKeys)
{
if (itemBytes.Length != itemKeys.Length)
{
throw new AbpException("count of the item bytes should be same with the count of the given keys");
}
var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();
for (int i = 0; i < itemKeys.Length; i++)
{
result.Add(
new KeyValuePair<TCacheKey, TCacheItem>(
itemKeys[i],
ToCacheItem(itemBytes[i])
)
);
}
return result.ToArray();
}
[CanBeNull]
protected virtual TCacheItem ToCacheItem([CanBeNull] byte[] bytes)
{
if (bytes == null)
{
return null;
}
return Serializer.Deserialize<TCacheItem>(bytes);
}
protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems(KeyValuePair<TCacheKey, TCacheItem>[] items)
{
return items
.Select(i => new KeyValuePair<string, byte[]>(
NormalizeKey(i.Key),
Serializer.Serialize(i.Value)
)
).ToArray();
}
private static KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItemsWithDefaultValues(TCacheKey[] keys)
{
return keys
.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(key, default))
.ToArray();
}
}
}

30
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
namespace Volo.Abp.Caching
{
public interface ICacheSupportsMultipleItems
{
byte[][] GetMany(
IEnumerable<string> keys
);
Task<byte[][]> GetManyAsync(
IEnumerable<string> keys,
CancellationToken token = default
);
void SetMany(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options
);
Task SetManyAsync(
IEnumerable<KeyValuePair<string, byte[]>> items,
DistributedCacheEntryOptions options,
CancellationToken token = default
);
}
}

63
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -34,6 +35,39 @@ namespace Volo.Abp.Caching
TCacheKey key,
bool? hideErrors = null
);
/// <summary>
/// Gets multiple cache items with the given keys.
///
/// The returned list contains exactly the same count of items specified in the given keys.
/// An item in the return list can not be null, but an item in the list has null value
/// if the related key not found in the cache.
/// </summary>
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <returns>List of cache items.</returns>
KeyValuePair<TCacheKey, TCacheItem>[] GetMany(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null
);
/// <summary>
/// Gets multiple cache items with the given keys.
///
/// The returned list contains exactly the same count of items specified in the given keys.
/// An item in the return list can not be null, but an item in the list has null value
/// if the related key not found in the cache.
///
/// </summary>
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// /// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>List of cache items.</returns>
Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync(
IEnumerable<TCacheKey> keys,
bool? hideErrors = null,
CancellationToken token = default
);
/// <summary>
/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
@ -113,6 +147,35 @@ namespace Volo.Abp.Caching
CancellationToken token = default
);
/// <summary>
/// Sets multiple cache items.
/// Based on the implementation, this can be more efficient than setting multiple items individually.
/// </summary>
/// <param name="items">Items to set on the cache</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
void SetMany(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null
);
/// <summary>
/// Sets multiple cache items.
/// Based on the implementation, this can be more efficient than setting multiple items individually.
/// </summary>
/// <param name="items">Items to set on the cache</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
Task SetManyAsync(
IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
DistributedCacheEntryOptions options = null,
bool? hideErrors = null,
CancellationToken token = default
);
/// <summary>
/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
/// </summary>

2
framework/src/Volo.Abp.Castle.Core/Volo.Abp.Castle.Core.csproj

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.4.0" />
<PackageReference Include="Castle.Core" Version="4.4.1" />
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.0.21-alpha" />
</ItemGroup>

8
framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj

@ -16,11 +16,11 @@
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="HtmlAgilityPack.NetCore" Version="1.5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="NuGet.Versioning" Version="5.4.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
<PackageReference Include="NuGet.Versioning" Version="5.6.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.1" />
<PackageReference Include="System.Security.Permissions" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Polly" Version="7.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Polly" Version="7.2.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
</ItemGroup>

38
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -51,17 +51,17 @@ namespace Volo.Abp.Cli.Commands
var apiUrl = commandLineArgs.Options.GetOrNull(Options.ApiUrl.Short, Options.ApiUrl.Long);
if (string.IsNullOrWhiteSpace(apiUrl))
{
var environmentJson = File.ReadAllText("src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " ");
var environmentJson = File.ReadAllText("projects/dev-app/src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " ");
var environment = JObject.Parse(environmentJson);
apiUrl = environment["apis"]["default"]["url"].ToString();
}
apiUrl += "/api/abp/api-definition?IncludeTypes=true";
apiUrl = apiUrl.EnsureEndsWith('/') + "api/abp/api-definition?IncludeTypes=true";
var uiFramework = GetUiFramework(commandLineArgs);
output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long);
if (!string.IsNullOrWhiteSpace(output) && !(output.EndsWith("/") || output.EndsWith("\\")))
{
{
output += "/";
}
@ -110,7 +110,7 @@ namespace Volo.Abp.Cli.Commands
if (rootPath != "app")
{
Logger.LogInformation($"{rootPath} directory is creating");
}
}
if (rootPath == "app")
{
@ -469,17 +469,21 @@ namespace Volo.Abp.Cli.Commands
if (returnValueType.Contains("<"))
{
returnValueType = returnValueType.Split('<')[1].Split('>')[0];
var clrType = Type.GetType(returnValueType, throwOnError: false);
if (clrType != null && TypeHelper.IsPrimitiveExtended(clrType, includeEnums: true))
{
return null;
}
if (returnValueType.StartsWith("Volo.Abp.Application.Dtos")
|| returnValueType.StartsWith("System.Collections")
|| returnValueType == "System.String"
|| returnValueType == "System.Void"
|| returnValueType.Contains("System.Net.HttpStatusCode?")
|| returnValueType.Contains("IActionResult")
|| returnValueType.Contains("ActionResult")
|| returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IValueValidator")
|| returnValueType.Contains("Guid")
)
|| returnValueType.StartsWith("System.Collections")
|| returnValueType == "System.Void"
|| returnValueType.Contains("System.Net.HttpStatusCode?")
|| returnValueType.Contains("IActionResult")
|| returnValueType.Contains("ActionResult")
|| returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IValueValidator")
)
{
return null;
}
@ -536,7 +540,7 @@ namespace Volo.Abp.Cli.Commands
modelFileText.AppendLine(Environment.NewLine);
modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';");
extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName);
var modelIndex = CreateType(data, baseType, rootPath, modelIndexList, controllerPathName);
@ -628,7 +632,7 @@ namespace Volo.Abp.Cli.Commands
{
from = "./" + propertyTypeKebabCase;
}
modelFileText.Insert(0, $"import {{ {propertyType} }} from '{from}';");
modelFileText.Insert(0, Environment.NewLine);
modelIndexList.Add(modelIndex);
@ -816,4 +820,4 @@ namespace Volo.Abp.Cli.Commands
public bool IsOptional { get; set; }
public string BindingSourceId { get; set; }
}
}
}

47
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs

@ -59,15 +59,33 @@ namespace Volo.Abp.Cli.Commands
return;
}
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
try
{
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
if (result == 0)
if (result == 0)
{
Logger.LogInformation("ABP Suite has been successfully installed.");
Logger.LogInformation("You can run it with the CLI command \"abp suite\"");
}
else
{
ShowSuiteManualInstallCommand();
}
}
catch (Exception e)
{
Logger.LogInformation("ABP Suite has been successfully installed.");
Logger.LogInformation("You can run it with the CLI command \"abp suite\"");
Logger.LogError("Couldn't install ABP Suite." + e.Message);
ShowSuiteManualInstallCommand();
}
}
private void ShowSuiteManualInstallCommand()
{
Logger.LogInformation("You can also run the following command to install ABP Suite.");
Logger.LogInformation("dotnet tool install -g Volo.Abp.Suite");
}
private async Task UpdateSuiteAsync()
{
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
@ -77,7 +95,26 @@ namespace Volo.Abp.Cli.Commands
return;
}
CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
try
{
var result = CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
if (result != 0)
{
ShowSuiteManualUpdateCommand();
}
}
catch (Exception ex)
{
Logger.LogError("Couldn't update ABP Suite." + ex.Message);
ShowSuiteManualUpdateCommand();
}
}
private void ShowSuiteManualUpdateCommand()
{
Logger.LogError("You can also run the following command to update ABP Suite.");
Logger.LogError("dotnet tool update -g Volo.Abp.Suite");
}
private static void RemoveSuite()

12
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs

@ -63,11 +63,13 @@ namespace Volo.Abp.Cli.Commands
solution = Directory.GetFiles(directory, "*.sln", SearchOption.AllDirectories).FirstOrDefault();
}
var checkAll = commandLineArgs.Options.ContainsKey(Options.CheckAll.Long);
if (solution != null)
{
var solutionName = Path.GetFileName(solution).RemovePostFix(".sln");
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(solution, includePreviews);
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(solution, includePreviews, checkAll: checkAll);
Logger.LogInformation($"Volo packages are updated in {solutionName} solution.");
return;
@ -79,7 +81,7 @@ namespace Volo.Abp.Cli.Commands
{
var projectName = Path.GetFileName(project).RemovePostFix(".csproj");
await _nugetPackagesVersionUpdater.UpdateProjectAsync(project, includePreviews);
await _nugetPackagesVersionUpdater.UpdateProjectAsync(project, includePreviews, checkAll: checkAll);
Logger.LogInformation($"Volo packages are updated in {projectName} project.");
return;
@ -107,6 +109,7 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine("--nuget (Only updates Nuget packages)");
sb.AppendLine("-sp|--solution-path (Specify the solution path)");
sb.AppendLine("-sn|--solution-name (Specify the solution name)");
sb.AppendLine("--check-all (Check the new version of each package separately)");
sb.AppendLine("");
sb.AppendLine("Some examples:");
sb.AppendLine("");
@ -149,6 +152,11 @@ namespace Volo.Abp.Cli.Commands
public const string Npm = "npm";
public const string NuGet = "nuget";
}
public static class CheckAll
{
public const string Long = "check-all";
}
}
}
}

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AngularEnvironmentFilePortChangeForSeparatedIdentityServersStep.cs

@ -10,9 +10,9 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App
{
var fileEntries = context.Files.Where(x =>
!x.IsDirectory &&
(x.Name.EndsWith("angular/src/environments/environment.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/src/environments/environment.hmr.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/src/environments/environment.prod.ts", StringComparison.InvariantCultureIgnoreCase))
(x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.hmr.ts", StringComparison.InvariantCultureIgnoreCase) ||
x.Name.EndsWith("angular/projects/dev-app/src/environments/environment.prod.ts", StringComparison.InvariantCultureIgnoreCase))
)
.ToList();

116
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -52,33 +53,35 @@ namespace Volo.Abp.Cli.ProjectModification
_npmGlobalPackagesChecker.Check();
foreach (var file in fileList)
var packagesUpdated = new ConcurrentDictionary<string, bool>();
async Task UpdateAsync(string file)
{
var packagesUpdated = await UpdatePackagesInFile(file, includePreviews, switchToStable);
var updated = await UpdatePackagesInFile(file, includePreviews, switchToStable);
packagesUpdated.TryAdd(file, updated);
}
if (packagesUpdated)
{
var fileDirectory = Path.GetDirectoryName(file).EnsureEndsWith(Path.DirectorySeparatorChar);
if (IsAngularProject(fileDirectory))
{
if (includePreviews)
{
await CreateNpmrcFileAsync(Path.GetDirectoryName(file));
}
else if (switchToStable)
{
await DeleteNpmrcFileAsync(Path.GetDirectoryName(file));
}
}
Task.WaitAll(fileList.Select(UpdateAsync).ToArray());
foreach (var file in packagesUpdated.Where(x => x.Value))
{
var fileDirectory = Path.GetDirectoryName(file.Key).EnsureEndsWith(Path.DirectorySeparatorChar);
if (includePreviews)
{
await CreateNpmrcFileAsync(Path.GetDirectoryName(file.Key));
RunNpmInstall(fileDirectory);
}
else if (switchToStable)
{
await DeleteNpmrcFileAsync(Path.GetDirectoryName(file.Key));
RunYarn(fileDirectory);
}
if (!IsAngularProject(fileDirectory))
{
Thread.Sleep(500);
RunGulp(fileDirectory);
}
if (!IsAngularProject(fileDirectory))
{
Thread.Sleep(1000);
RunGulp(fileDirectory);
}
}
}
@ -161,7 +164,8 @@ namespace Volo.Abp.Cli.ProjectModification
return File.Exists(Path.Combine(fileDirectory, "angular.json"));
}
protected virtual async Task<bool> UpdatePackagesInFile(string filePath, bool includePreviews = false, bool switchToStable = false)
protected virtual async Task<bool> UpdatePackagesInFile(string filePath, bool includePreviews = false,
bool switchToStable = false)
{
var packagesUpdated = false;
var fileContent = File.ReadAllText(filePath);
@ -196,67 +200,54 @@ namespace Volo.Abp.Cli.ProjectModification
bool includePreviews = false,
bool switchToStable = false)
{
var currentVersion = (string)package.Value;
var version = await GetLatestVersion(package, currentVersion, includePreviews, switchToStable);
var currentVersion = (string) package.Value;
var versionWithPrefix = $"~{version}";
var version = "";
if (includePreviews || (!switchToStable && currentVersion.Contains("-preview")))
{
version = "preview";
}
else
{
version = await GetLatestVersion(package);
}
if (versionWithPrefix == currentVersion)
if (version == currentVersion)
{
return false;
}
package.Value.Replace(versionWithPrefix);
package.Value.Replace(version);
Logger.LogInformation($"Updated {package.Name} to {version} in {filePath.Replace(Directory.GetCurrentDirectory(), "")}.");
Logger.LogInformation(
$"Updated {package.Name} to {version} in {filePath.Replace(Directory.GetCurrentDirectory(), "")}.");
return true;
}
protected virtual async Task<string> GetLatestVersion(
JProperty package,
string currentVersion,
bool includePreviews = false,
bool switchToStable = false)
JProperty package)
{
if (_fileVersionStorage.ContainsKey(package.Name))
{
return _fileVersionStorage[package.Name];
}
var newVersion = currentVersion;
if (includePreviews || (!switchToStable && currentVersion.Contains("-preview")))
{
if (_myGetApiResponse == null)
{
_myGetApiResponse = await _myGetPackageListFinder.GetPackagesAsync();
}
var mygetPackage = _myGetApiResponse.Packages.FirstOrDefault(p => p.Id == package.Name);
if (mygetPackage != null)
{
newVersion = mygetPackage.Versions.Last();
}
}
else
{
newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version");
}
var newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version");
var newVersionWithPrefix = $"~{newVersion}";
_fileVersionStorage[package.Name] = newVersion;
_fileVersionStorage[package.Name] = newVersionWithPrefix;
return newVersion;
return $"~{newVersionWithPrefix}";
}
protected virtual List<JProperty> GetAbpPackagesFromPackageJson(JObject fileObject)
{
var dependencyList = new[] { "dependencies", "devDependencies", "peerDependencies" };
var dependencyList = new[] {"dependencies", "devDependencies", "peerDependencies"};
var abpPackages = new List<JProperty>();
foreach (var dependencyListName in dependencyList)
{
var dependencies = (JObject)fileObject[dependencyListName];
var dependencies = (JObject) fileObject[dependencyListName];
if (dependencies == null)
{
@ -264,7 +255,8 @@ namespace Volo.Abp.Cli.ProjectModification
}
var properties = dependencies.Properties().ToList();
abpPackages.AddRange(properties.Where(p => p.Name.StartsWith("@abp/") || p.Name.StartsWith("@volo/")).ToList());
abpPackages.AddRange(properties.Where(p => p.Name.StartsWith("@abp/") || p.Name.StartsWith("@volo/"))
.ToList());
}
return abpPackages;
@ -281,5 +273,11 @@ namespace Volo.Abp.Cli.ProjectModification
Logger.LogInformation($"Running Yarn on {fileDirectory}");
CmdHelper.RunCmd($"cd {fileDirectory} && yarn");
}
protected virtual void RunNpmInstall(string fileDirectory)
{
Logger.LogInformation($"Running npm install on {fileDirectory}");
CmdHelper.RunCmd($"cd {fileDirectory} && npm install");
}
}
}

49
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs

@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using NuGet.Versioning;
using System.IO;
using System.Linq;
@ -24,19 +25,47 @@ namespace Volo.Abp.Cli.ProjectModification
Logger = NullLogger<VoloNugetPackagesVersionUpdater>.Instance;
}
public async Task UpdateSolutionAsync(string solutionPath, bool includePreviews = false, bool switchToStable = false)
public async Task UpdateSolutionAsync(string solutionPath, bool includePreviews = false, bool switchToStable = false, bool checkAll = false)
{
var projectPaths = ProjectFinder.GetProjectFiles(solutionPath);
foreach (var filePath in projectPaths)
if (checkAll)
{
await UpdateInternalAsync(filePath, includePreviews, switchToStable);
Task.WaitAll(projectPaths.Select(projectPath => UpdateInternalAsync(projectPath, includePreviews, switchToStable)).ToArray());
}
else
{
var latestVersionFromNuget = await _nuGetService.GetLatestVersionOrNullAsync("Volo.Abp.Core");
var latestVersionFromMyGet = await GetLatestVersionFromMyGet("Volo.Abp.Core");
async Task UpdateAsync(string filePath)
{
var fileContent = File.ReadAllText(filePath);
var updatedContent = await UpdateVoloPackagesAsync(fileContent, includePreviews, switchToStable, latestVersionFromNuget, latestVersionFromMyGet);
File.WriteAllText(filePath, updatedContent);
}
Task.WaitAll(projectPaths.Select(UpdateAsync).ToArray());
}
}
public async Task UpdateProjectAsync(string projectPath, bool includePreviews = false, bool switchToStable = false)
public async Task UpdateProjectAsync(string projectPath, bool includePreviews = false, bool switchToStable = false, bool checkAll = false)
{
await UpdateInternalAsync(projectPath, includePreviews, switchToStable);
if (checkAll)
{
await UpdateInternalAsync(projectPath, includePreviews, switchToStable);
}
else
{
var latestVersionFromNuget = await _nuGetService.GetLatestVersionOrNullAsync("Volo.Abp.Core");
var latestVersionFromMyGet = await GetLatestVersionFromMyGet("Volo.Abp.Core");
var fileContent = File.ReadAllText(projectPath);
var updatedContent = await UpdateVoloPackagesAsync(fileContent, includePreviews, switchToStable, latestVersionFromNuget, latestVersionFromMyGet);
File.WriteAllText(projectPath, updatedContent);
}
}
protected virtual async Task UpdateInternalAsync(string projectPath, bool includePreviews = false, bool switchToStable = false)
@ -47,7 +76,7 @@ namespace Volo.Abp.Cli.ProjectModification
File.WriteAllText(projectPath, updatedContent);
}
private async Task<string> UpdateVoloPackagesAsync(string content, bool includePreviews = false, bool switchToStable = false)
private async Task<string> UpdateVoloPackagesAsync(string content, bool includePreviews = false, bool switchToStable = false, SemanticVersion latestNugetVersion = null, string latestMyGetVersion = null)
{
string packageId = null;
@ -81,7 +110,7 @@ namespace Volo.Abp.Cli.ProjectModification
if (includePreviews || (currentVersion.Contains("-preview") && !switchToStable))
{
var latestVersion = await GetLatestVersionFromMyGet(packageId);
var latestVersion = latestMyGetVersion ?? await GetLatestVersionFromMyGet(packageId);
if (currentVersion != latestVersion)
{
@ -95,7 +124,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
else
{
var latestVersion = await _nuGetService.GetLatestVersionOrNullAsync(packageId);
var latestVersion = latestNugetVersion ?? await _nuGetService.GetLatestVersionOrNullAsync(packageId);
if (latestVersion != null && (currentVersion.Contains("-preview") || currentSemanticVersion < latestVersion))
{
@ -109,7 +138,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
return await Task.FromResult(doc.OuterXml);
return doc.OuterXml;
}
}
catch (Exception ex)

7
framework/src/Volo.Abp.Cli/Properties/launchSettings.json

@ -1,7 +0,0 @@
{
"profiles": {
"Volo.Abp.Cli": {
"commandName": "Project"
}
}
}

4
framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj

@ -18,8 +18,8 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" />
</ItemGroup>
<ItemGroup>

9
framework/src/Volo.Abp.Core/System/Runtime/IOSPlatformProvider.cs

@ -0,0 +1,9 @@
using System.Runtime.InteropServices;
namespace System.Runtime
{
public interface IOSPlatformProvider
{
OSPlatform GetCurrentOSPlatform();
}
}

23
framework/src/Volo.Abp.Core/System/Runtime/OSPlatformProvider.cs

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using Volo.Abp.DependencyInjection;
namespace System.Runtime
{
public class OSPlatformProvider : IOSPlatformProvider, ITransientDependency
{
public virtual OSPlatform GetCurrentOSPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return OSPlatform.OSX; //MAC
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return OSPlatform.Windows;
}
return OSPlatform.Linux;
}
}
}

24
framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj

@ -15,20 +15,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.19" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.1.5" />
<PackageReference Include="System.Linq.Queryable" Version="4.3.0" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.0.0" />
<PackageReference Include="Nito.AsyncEx.Context" Version="5.0.0" />
</ItemGroup>

2
framework/src/Volo.Abp.Core/Volo/Abp/Collections/ITypeList.cs

@ -27,7 +27,7 @@ namespace Volo.Abp.Collections
/// Adds a type to list if it's not already in the list.
/// </summary>
/// <typeparam name="T">Type</typeparam>
void TryAdd<T>() where T : TBaseType;
bool TryAdd<T>() where T : TBaseType;
/// <summary>
/// Checks if a type exists in the list.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save