Browse Source

Merge branch 'dev' into issue/18074

pull/18111/head
Mahmut Gundogdu 3 years ago
parent
commit
7243c1fa85
  1. 16
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  2. 2
      docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md
  3. 124
      docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md
  4. 156
      docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md
  5. 79
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md
  6. BIN
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png
  7. 75
      docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md
  8. 4
      docs/en/Startup-Templates/Application.md
  9. 8
      docs/en/Tutorials/Part-5.md
  10. 11
      docs/en/UI/Angular/Entity-Action-Extensions.md
  11. 1
      docs/en/UI/Angular/OAuth-Module.md
  12. 8
      docs/en/UI/Angular/Permission-Management.md
  13. 77
      docs/en/UI/AspNetCore/Bundling-Minification.md
  14. 4
      docs/zh-Hans/Startup-Templates/Application.md
  15. 8
      docs/zh-Hans/Tutorials/Part-5.md
  16. 10
      docs/zh-Hans/UI/Angular/Permission-Management.md
  17. 77
      docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md
  18. 1
      framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs
  19. 1
      framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs
  20. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs
  21. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs
  22. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs
  23. 31
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs
  24. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs
  25. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs
  26. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs
  27. 50
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs
  28. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs
  29. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs
  30. 34
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs
  31. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
  32. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
  33. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs
  34. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs
  35. 7
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs
  36. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs
  37. 22
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs
  38. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs
  39. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
  40. 60
      framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css
  41. 1
      framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj
  42. 44
      framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs
  43. 3
      framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs
  44. 10
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  45. 37
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs
  46. 6
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs
  47. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  48. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  49. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  50. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  51. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  52. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  53. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  54. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  55. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  56. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  57. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  58. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  59. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  60. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  61. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  62. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  63. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  64. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  65. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json
  66. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json
  67. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json
  68. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json
  69. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json
  70. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  71. 51
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs
  72. 11
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs
  73. 168
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs
  74. 2
      latest-versions.json
  75. 43
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  76. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml
  77. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  78. 25
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  79. 13
      npm/ng-packs/packages/components/extensible/src/lib/models/entity-props.ts
  80. 4
      npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts
  81. 117
      npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts
  82. 65
      npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts
  83. 4
      npm/ng-packs/packages/schematics/src/index.ts

16
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json

@ -601,7 +601,7 @@
"PaymentSucceed_ViewOrganization": "Click here to view organization",
"Purchase_TotalAnnualPrice": "TOTAL <small class=\"opacity-50\">(annual fee)</small>",
"Purchase_TrainingPrice": "Training Price",
"Purchase_OnboardingTraining": "ABP Onboarding & Web Application Development Live Training",
"Purchase_OnboardingTraining": "Onboarding & Web Application Development Live Training",
"TotalDeveloperPrice": "Total Developer Price",
"Purchase_PricePerDeveloper": "<span>{0} {1}</span> per developer",
"Purchase_IncludedDeveloperInfo": "{0} {1} included.",
@ -978,10 +978,10 @@
"Purchase_DevelopersAlreadyIncluded": "{0} developers already included",
"1Year": "1 year",
"{0}Years": "{0} years",
"1YearLicense": "1 year license",
"{0}YearsLicense": "{0} years license",
"1AdditionalDeveloper": "1 additional developer",
"{0}AdditionalDevelopers": "{0} additional developers",
"1YearLicense": "1 Year License",
"{0}YearsLicense": "{0} Years License",
"1AdditionalDeveloper": "1 Additional Developer",
"{0}AdditionalDevelopers": "{0} Additional Developers",
"Discount": "Discount ({0}%)",
"Summary": "Summary",
"TrainingPack": "Training pack",
@ -1081,6 +1081,10 @@
"CampaignInfo": "Buy a new license or renew your existing license and <span class=\"text-white\">get an additional 2 months</span> at no additional cost! This offer is valid for all license plans. Ensure you take advantage of this limited-time promotion to expand your access to premium features and upgrades.",
"HurryUpLastDay": "Hurry Up! Last Day: {0}",
"CreatingCRUDPagesWithABPSuite": "Creating CRUD pages with ABP Suite",
"Testimonials": "Testimonials"
"Testimonials": "Testimonials",
"MultipleYearDiscount": "Multiple Year Discount",
"CampaignDiscountText": "Black Friday Discount",
"CampaignDiscountName": "Black Friday",
"CampaignName:BlackFriday": "Black Friday"
}
}

2
docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md

@ -315,7 +315,7 @@ import { CreateBookComponent } from './create-book/create-book.component';
import { EditBookComponent } from './edit-book/edit-book.component';
const routes: Routes = [
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
{ path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] },
{ path: 'create', component: CreateBookComponent },
{ path: 'edit/:id', component: EditBookComponent },
];

124
docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md

@ -0,0 +1,124 @@
# EF Core 8 - Enhancements to JSON column mapping
In this article, we will examine the enhancements introduced in EF Core 8 for the JSON column feature, building upon the foundation laid by [JSON columns in Entity Framework Core 7](https://community.abp.io/posts/json-columns-in-entity-framework-core-7-cjaom76j).
## The entity classes we will be using in the article
```csharp
public class Person
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public ContactDetails ContactDetails { get; set; }
}
public class ContactDetails
{
public List<Address> Addresses { get; set; } = new();
public string? Phone { get; set; }
}
public class Address
{
public Address(string street, string city, string postcode, string country)
{
Street = street;
City = city;
Postcode = postcode;
Country = country;
}
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public string Country { get; set; }
public bool IsMainAddress { get; set; }
}
```
## The DbContext class we will be using in the article
```csharp
public class AppDbContext : DbContext
{
public DbSet<Person> Persons { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
#if SQLSERVER
optionsBuilder.UseSqlServer("Server=localhost;Database=EfCore8Json;Trusted_Connection=True;TrustServerCertificate=True");
#elif SQLITE
optionsBuilder.UseSqlite("Data Source=EfCore8Json.db");
#endif
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>(b =>
{
b.ToTable("Persons");
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
b.OwnsOne(x => x.ContactDetails, cb =>
{
cb.ToJson();
cb.Property(x => x.Phone);
cb.OwnsMany(x => x.Addresses);
});
});
base.OnModelCreating(modelBuilder);
}
}
```
## Translate element access into JSON arrays
EF Core 8 supports indexing in JSON arrays when executing queries. For example, the following query returns individuals whose first address is the main address in the database:
```csharp
var query = dbContext.Persons
.Select(x => x.ContactDetails.Addresses[0])
.Where(x => x.IsMainAddress == true)
.ToListAsync();
```
The generated SQL query is as follows when using SQL Server:
```sql
SELECT JSON_QUERY([p].[ContactDetails], '$.Addresses[0]'), [p].[Id]
FROM [Persons] AS [p]
WHERE CAST(JSON_VALUE([p].[ContactDetails], '$.Addresses[0].IsMainAddress') AS bit) = CAST(1 AS bit)
```
> Note: If you attempt to access an index that is outside of the array, it will return null.
## JSON Columns for SQLite
In EF Core 7, JSON column mapping was supported for Azure SQL/SQL Server. In EF Core 8, this support has been extended to include SQLite as well.
### Queries into JSON columns
The following query returns individuals whose first address is the main address in the database:
```csharp
var query = dbContext.Persons
.Select(x => x.ContactDetails.Addresses[0])
.Where(x => x.IsMainAddress == true)
.ToListAsync();
```
The generated SQL query is as follows when using SQLite:
```sql
SELECT "p"."ContactDetails" ->> '$.Addresses[0]', "p"."Id"
FROM "Persons" AS "p"
WHERE "p"."ContactDetails" ->> '$.Addresses[0].IsMainAddress' = 0
```
## References
- [EF Core 8 - Enhancements to JSON column mapping](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#enhancements-to-json-column-mapping)

156
docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md

@ -0,0 +1,156 @@
# New Minimal APIs features in ASP.NET Core 8.0
In this article, we will see the new features of Minimal APIs in ASP.NET Core 8.0.
## Binding to forms
We can bind to forms using the [FromForm] attribute. Let's see an example:
```csharp
app.MapPost("/books", async ([FromForm] string name,
[FromForm] BookType bookType, IFormFile? cover, BookDb db) =>
{
var book = new Book
{
Name = name,
BookType = bookType
};
if (cover is not null)
{
var coverName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", coverName));
await cover.CopyToAsync(stream);
book.Cover = coverName;
}
db.Books.Add(book);
await db.SaveChangesAsync();
return Results.Ok();
});
```
Another way is using the [AsParameters] attribute, the following code binds from form values to properties of the `NewBookRequest` record struct:
```csharp
public record NewBookRequest([FromForm] string Name, [FromForm] BookType BookType, IFormFile? Cover);
app.MapPost("/books", async ([AsParameters] NewBookRequest request, BookDb db) =>
{
var book = new Book
{
Name = request.Name,
BookType = request.BookType
};
if (request.Cover is not null)
{
var coverName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", coverName));
await request.Cover.CopyToAsync(stream);
book.Cover = coverName;
}
db.Books.Add(book);
await db.SaveChangesAsync();
return Results.Ok();
});
```
## Antiforgery
ASP.NET Core 8.0 adds support for antiforgery tokens. We can call the `AddAntiforgery` method to register the antiforgery services and `WebApplicationBuilder` will automatically add the antiforgery middleware to the pipeline:
```csharp
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
// Implicitly added by WebApplicationBuilder if AddAntiforgery is called.
// app.UseAntiforgery();
app.MapGet("/", () => "Hello World!");
app.Run();
```
Example of using antiforgery tokens:
```csharp
// Use the antiforgery service to generate tokens.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
return Results.Content(...., "text/html");
});
// It will automatically validate the token.
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));
// Disable antiforgery validation for this endpoint.
app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
.DisableAntiforgery();
```
## Native AOT
ASP.NET Core 8.0 adds support for Native AOT.
You can add `PublishAot` to the project file to enable Native AOT:
```xml
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
```
### Web API (native AOT) template
You can use the `dotnet new webapiaot` command to create a new Minimal APIs project with Native AOT enabled.
```diff
+using System.Text.Json.Serialization;
-var builder = WebApplication.CreateBuilder();
+var builder = WebApplication.CreateSlimBuilder(args);
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+});
var app = builder.Build();
var sampleTodos = TodoGenerator.GenerateTodos().ToArray();
var todosApi = app.MapGroup("/todos");
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) =>
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
app.Run();
+[JsonSerializable(typeof(Todo[]))]
+internal partial class AppJsonSerializerContext : JsonSerializerContext
+{
+
+}
```
* Reflection isn't supported in native AOT, you must use the `JsonSerializable` attribute to specify the types that you want to serialize/deserialize.
## References
* https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#minimal-apis
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#explicit-binding-from-form-values
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0

79
docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md

@ -0,0 +1,79 @@
# HierarchyId Support in Entity Framework Core
Entity Framework Core has official support for HierarchyId, which allows you to store and query hierarchical data in SQL Server databases. Hierarchical data is a common data structure found in many applications. Whether you are dealing with organizational structures, product categories, or threaded discussions, handling hierarchies efficiently is crucial. In this blog post, we will explore how to manage hierarchical data using Entity Framework Core (EF Core) in combination with HierarchyId.
## How to use HierarchyId in EF Core
To use HierarchyId in EF Core, you need to install the [Microsoft.EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.HierarchyId/) NuGet package. This package contains query and update support for HierarchyId.
To configure EF Core to use HierarchyId, you need to call the `UseHierarchyId` method when configuring the SQL Server provider.
```csharp
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
```
## Modeling Hierarchies
The `HierarchyId` type can be used for properties of an entity type. For example, assume we want to model the personnel in an organization. Each person has a name and a manager. The manager is also a person. We can model this using the following entity type:
```csharp
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public HierarchyId Manager { get; set; }
}
```
Each person can be traced from the patriarch down the tree using its `Manager` property. SQL Server uses a compact binary format for these paths, but it is common to parse to and from a human-readable string representation when working with code. In this representation, the position at each level is separated by a `/` character.
![Hierarchy Tree](hierarchy-tree.png)
## Querying Hierarchies
HierarchyId exposes several methods that can be used in LINQ queries.
* `GetAncestor(int level)` returns the ancestor at the specified level.
* `GetDescendant(HierarchyId? child1, HierarchyId? child2)` returns the descendant at the specified level.
* `GetLevel()` returns the level of the node in the hierarchy.
* `GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot)` returns the node with a new parent.
* `IsDescendantOf(HierarchyId? parent)` returns whether the node is a descendant of the specified node.
Example:
```csharp
// Get entities at a given level in the tree
var generation = await context.Persons.Where(x => x.Manager.GetLevel() == level).ToListAsync();
// Get the direct ancestor of an entity
var parent = await context.Persons.Where(x => x.Manager.GetAncestor(1) == manager).SingleAsync();
```
For example, your company may want to change the manager of an organizational unit. To do this, you can use the `GetReparentedValue` method to update the manager for all descendants of the organizational unit.
```csharp
var newManager = await context.Persons.SingleAsync(x => x.Id == newManagerId);
var descendants = await context.Persons
.Where(x => x.Manager.IsDescendantOf(oldManager))
.ToListAsync();
foreach (var descendant in descendants)
{
descendant.Manager = descendant.Manager.GetReparentedValue(oldManager.Manager, newManager.Manager);
}
await context.SaveChangesAsync();
```
## References
For more information about hierarchy id, see the following resource:
https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#hierarchyid-in-net-and-ef-core

BIN
docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

75
docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md

@ -0,0 +1,75 @@
# Native AOT Compilation in .NET 8
Native AOT (Ahead-of-Time) compilation is a feature that allows developers to create a self-contained app compiled to native code that can run on machines without the .NET runtime installed. It results in benefits such as minimized disk footprint, reduced executable size, reduced startup time, and reduced memory demand.
Native AOT compilation isn't a new feature in .NET 8. It's first introduced in .NET 7.
Differences between the AOT Compilation of .NET 7 and .NET 8 are:
- **System.Text.Json improvements**: .NET 8 adds support for more types, source generation, interface hierarchies, naming policies, read-only properties, and more.
- **New types for performance**: .NET 8 introduces new types such as FrozenDictionary, FrozenSet, SearchValues, CompositeFormat, TimeProvider, and ITimer to improve the app performance.
- **System.Numerics and System.Runtime.Intrinsics enhancements**: .NET 8 adds support for Vector512, AVX-512, IUtf8SpanFormattable, Lerp, and more.
- **System.ComponentModel.DataAnnotations additions**: .NET 8 adds new data validation attributes for cloud-native services and a new ValidateOptionsResultBuilder type.
- **Hosted services lifecycle methods**: .NET 8 adds new methods such as StartAsync, StopAsync, StartBackgroundAsync, and StopBackgroundAsync for hosted services.
It's important to note that not all features in ASP.NET Core are currently compatible with native AOT. For more information, see [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/).
## How to use Native AOT Compilation in .NET 8
You can add `<PublishAot>true</PublishAot>` in your project .csproj file to enable Native AOT Compilation.
- For the new projects, you can create them with the `--aot` parameter. Example: `dotnet new console --aot`.
By default, the compiler chooses a blended approach code optimization but you can specify an optimization preference inside your .csproj file. You can choose **size** or **speed** according your requirements.
```xml
<OptimizationPreference>Size</OptimizationPreference>
```
or
```xml
<OptimizationPreference>Speed</OptimizationPreference>
```
### Results
I have created a simple console application to test the Native AOT Compilation. I have used a simple console application that writes "Hello World!" to the console 100 times. I have tested the application with different optimization preferences. I have used the following results:
| | Size | Speed |
| --- | --- | --- |
| .NET 8 <br/>_(Self-Contained, Single File)_ | 65938 kb | 00.0051806 ~5ms |
| .NET 7 AOT (default) | 4452 kb | 00.0029823 ~2ms |
| .NET 8 AOT (default) | 1242 kb | 00.0028638 ~2ms |
| AOT (Speed)| 1280 kb | 00.0023838 ~2ms |
| AOT (Size) | 1111 kb | 00.0025145 ~2ms |
Most of existing libraries don't support AOT compilation yet, so I couldn't use [BenchmarkDotnet](https://github.com/dotnet/BenchmarkDotNet) to measure the performance. I have used [Stopwatch](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-8.0) to measure the performance. So the performance results may not be accurate but gives insight about the performance difference.
## AOT Support in MAUI
You can now use Native AOT Compilation on iOS-like target frameworks in .NET MAUI. You can enable AOT compilation with the exact same method by adding `<PublishAot>true</PublishAot>` to your project .csproj file. According to the dotnet team, apps sizes reduced by 35% and startup times reduced by 28% with AOT compilation. And runtime performance is also improved by 50%.
But there are some limitations in MAUI AOT Compilation. A lot of libraries still don't support AOT compilation and some of platform-specific feaetures may not work at the moment.
## When to use Native AOT Compilation?
Native AOT Compilation is beneficial when you need to optimize your .NET application for speed and size. It's particularly useful for applications that require quick startup times and efficient runtime performance, such as mobile apps or high-performance computing applications.
However, due to its current limitations, it might not be suitable for all projects. If your project relies heavily on libraries that do not support AOT compilation, or if it uses platform-specific features that are not yet compatible with AOT, you might want to hold off on using Native AOT Compilation until further improvements are made.
Always consider the specific needs and constraints of your project before deciding to use Native AOT Compilation.
## Conclusion
Native AOT Compilation is a great feature that improves the performance of .NET applications. It's still in early-stages and not all libraries support it yet. But it's a great beginning for the future of .NET 🚀
## Links
- Native AOT deployment overview - .NET | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/.
-
- Optimize AOT deployments https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/optimizing
-
- What's new in .NET 8 | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8.

4
docs/en/Startup-Templates/Application.md

@ -316,7 +316,7 @@ You should add `routes` property in the `data` object to add a link on the menu
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [AuthGuard, PermissionGuard],
canActivate: [authGuard, permissionGuard],
data: {
routes: {
name: 'ProjectName::Menu:Dashboard',
@ -328,7 +328,7 @@ You should add `routes` property in the `data` object to add a link on the menu
}
```
In the above example;
* If the user is not logged in, AuthGuard blocks access and redirects to the login page.
* If the user is not logged in, authGuard blocks access and redirects to the login page.
* PermissionGuard checks the user's permission with the `requiredPolicy` property of the `routes` object. If the user is not authorized to access the page, the 403 page appears.
* The `name` property of `routes` is the menu link label. A localization key can be defined.
* The `iconClass` property of the `routes` object is the menu link icon class.

8
docs/en/Tutorials/Part-5.md

@ -323,11 +323,11 @@ Open the `/src/app/book/book-routing.module.ts` and replace with the following c
````js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';
import { authGuard, permissionGuard } from '@abp/ng.core';
import { BookComponent } from './book.component';
const routes: Routes = [
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
{ path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] },
];
@NgModule({
@ -337,8 +337,8 @@ const routes: Routes = [
export class BookRoutingModule {}
````
* Imported `AuthGuard` and `PermissionGuard` from the `@abp/ng.core`.
* Added `canActivate: [AuthGuard, PermissionGuard]` to the route definition.
* Imported `authGuard` and `permissionGuard` from the `@abp/ng.core`.
* Added `canActivate: [authGuard, permissionGuard]` to the route definition.
Open the `/src/app/route.provider.ts` and add `requiredPolicy: 'BookStore.Books'` to the `/books` route. The `/books` route block should be following:

11
docs/en/UI/Angular/Entity-Action-Extensions.md

@ -16,19 +16,16 @@ In this example, we will add a "Click Me!" action and alert the current row's `u
The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module:
```js
```ts
// src/app/entity-action-contributors.ts
import {
eIdentityComponents,
IdentityEntityActionContributors,
IdentityUserDto,
} from '@abp/ng.identity';
import { eIdentityComponents, IdentityEntityActionContributors } from '@abp/ng.identity';
import { IdentityUserDto } from '@abp/ng.identity/proxy';
import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions';
const alertUserName = new EntityAction<IdentityUserDto>({
text: 'Click Me!',
action: data => {
action: (data) => {
// Replace alert with your custom code
alert(data.record.userName);
},

1
docs/en/UI/Angular/OAuth-Module.md

@ -6,7 +6,6 @@ If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot
Those abstractions can be found in the @abp/ng-core packages.
- `AuthService` (the class that implements the IAuthService interface).
- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
- `AuthGuard` (the class that implements the IAuthGuard interface).
- `ApiInterceptor` (the class that implements the IApiInterceptor interface).
Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule.

8
docs/en/UI/Angular/Permission-Management.md

@ -54,20 +54,20 @@ As shown above you can remove elements from DOM with `abpPermission` structural
## Permission Guard
You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation.
You can use `permissionGuard` if you want to control authenticated user's permission to access to the route during navigation.
* Import the PermissionGuard from @abp/ng.core.
* Add `canActivate: [PermissionGuard]` to your route object.
* Add `canActivate: [permissionGuard]` to your route object.
* Add `requiredPolicy` to the `data` property of your route in your routing module.
```js
import { PermissionGuard } from '@abp/ng.core';
import { permissionGuard } from '@abp/ng.core';
// ...
const routes: Routes = [
{
path: 'path',
component: YourComponent,
canActivate: [PermissionGuard],
canActivate: [permissionGuard],
data: {
requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component
},

77
docs/en/UI/AspNetCore/Bundling-Minification.md

@ -382,6 +382,83 @@ Configure<AbpBundlingOptions>(options =>
<script defer src="/libs/timeago/locales/jquery.timeago.en.js?_v=637674729040000000"></script>
````
### External/CDN file Support
The bundling system automatically recognizes the external/CDN files and adds them to the page without any change.
#### Using External/CDN files in `AbpBundlingOptions`
````csharp
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles
.Add("MyStyleBundle", configuration =>
{
configuration
.AddFiles("/styles/my-style1.css")
.AddFiles("/styles/my-style2.css")
.AddFiles("https://cdn.abp.io/bootstrap.css")
.AddFiles("/styles/my-style3.css")
.AddFiles("/styles/my-style4.css");
});
options.ScriptBundles
.Add("MyScriptBundle", configuration =>
{
configuration
.AddFiles("/scripts/my-script1.js")
.AddFiles("/scripts/my-script2.js")
.AddFiles("https://cdn.abp.io/bootstrap.js")
.AddFiles("/scripts/my-script3.js")
.AddFiles("/scripts/my-script4.js");
});
});
````
**Output HTML:**
````html
<link rel="stylesheet" href="/__bundles/MyStyleBundle.EA8C28419DCA43363E9670973D4C0D15.css?_v=638331889644609730" />
<link rel="stylesheet" href="https://cdn.abp.io/bootstrap.css" />
<link rel="stylesheet" href="/__bundles/MyStyleBundle.AC2E0AA6C461A0949A1295E9BDAC049C.css?_v=638331889644623860" />
<script src="/__bundles/MyScriptBundle.C993366DF8840E08228F3EE685CB08E8.js?_v=638331889644937120"></script>
<script src="https://cdn.abp.io/bootstrap.js"></script>
<script src="/__bundles/MyScriptBundle.2E8D0FDC6334D2A6B847393A801525B7.js?_v=638331889644943970"></script>
````
#### Using External/CDN files in Tag Helpers.
````html
<abp-style-bundle name="MyStyleBundle">
<abp-style src="/styles/my-style1.css" />
<abp-style src="/styles/my-style2.css" />
<abp-style src="https://cdn.abp.io/bootstrap.css" />
<abp-style src="/styles/my-style3.css" />
<abp-style src="/styles/my-style4.css" />
</abp-style-bundle>
<abp-script-bundle name="MyScriptBundle">
<abp-script src="/scripts/my-script1.js" />
<abp-script src="/scripts/my-script2.js" />
<abp-script src="https://cdn.abp.io/bootstrap.js" />
<abp-script src="/scripts/my-script3.js" />
<abp-script src="/scripts/my-script4.js" />
</abp-script-bundle>
````
**Output HTML:**
````html
<link rel="stylesheet" href="/__bundles/MyStyleBundle.C60C7B9C1F539659623BB6E7227A7C45.css?_v=638331889645002500" />
<link rel="stylesheet" href="https://cdn.abp.io/bootstrap.css" />
<link rel="stylesheet" href="/__bundles/MyStyleBundle.464328A06039091534650B0E049904C6.css?_v=638331889645012300" />
<script src="/__bundles/MyScriptBundle.55FDCBF2DCB9E0767AE6FA7487594106.js?_v=638331889645050410"></script>
<script src="https://cdn.abp.io/bootstrap.js"></script>
<script src="/__bundles/MyScriptBundle.191CB68AB4F41C8BF3A7AE422F19A3D2.js?_v=638331889645055490"></script>
````
## Themes
Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more.

4
docs/zh-Hans/Startup-Templates/Application.md

@ -302,7 +302,7 @@ ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [AuthGuard, PermissionGuard],
canActivate: [authGuard, permissionGuard],
data: {
routes: {
name: 'ProjectName::Menu:Dashboard',
@ -315,7 +315,7 @@ ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP
```
在上面的例子中;
* 如果用户没有登录, AuthGuard 会阻塞访问并重定向到登录页面.
* PermissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页.
* permissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页.
* `routes` 的 `name` 属性是菜单链接标签. 可以定义本地化 key.
* `routes` 对象的 `iconClass` 属性是菜单链接图标类.
* `routes` 对象的 `requiredPolicy` 属性是访问页面所需的策略 key.

8
docs/zh-Hans/Tutorials/Part-5.md

@ -389,11 +389,11 @@ UI的第一步是防止未认证用户看见"图书"菜单项并进入图书管
````js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';
import { authGuard, permissionGuard } from '@abp/ng.core';
import { BookComponent } from './book.component';
const routes: Routes = [
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
{ path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] },
];
@NgModule({
@ -403,8 +403,8 @@ const routes: Routes = [
export class BookRoutingModule {}
````
* 从 `@abp/ng.core` 引入 `AuthGuard` 和 `PermissionGuard`.
* 在路由定义中添加 `canActivate: [AuthGuard, PermissionGuard]`.
* 从 `@abp/ng.core` 引入 `authGuard` 和 `permissionGuard`.
* 在路由定义中添加 `canActivate: [authGuard, permissionGuard]`.
打开 `/src/app/route.provider.ts`, 在 `/books` 路由中添加 `requiredPolicy: 'BookStore.Books'`. `/books` 路由应该如以下配置:

10
docs/zh-Hans/UI/Angular/Permission-Management.md

@ -53,20 +53,20 @@ export class YourComponent {
## 权限守卫
如果你想要在导航过程中控制经过身份验证的用户对路由的访问权限,可以使用 `PermissionGuard`.
如果你想要在导航过程中控制经过身份验证的用户对路由的访问权限,可以使用 `permissionGuard`.
* 从@abp/ng.core导入PermissionGuard.
* 添加 `canActivate: [PermissionGuard]` 到你的路由对象.
* 从@abp/ng.core导入permissionGuard.
* 添加 `canActivate: [permissionGuard]` 到你的路由对象.
* 添加 `requiredPolicy` 到路由模块路由的 `data` 属性.
```js
import { PermissionGuard } from '@abp/ng.core';
import { permissionGuard } from '@abp/ng.core';
// ...
const routes: Routes = [
{
path: 'path',
component: YourComponent,
canActivate: [PermissionGuard],
canActivate: [permissionGuard],
data: {
requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component
},

77
docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md

@ -353,6 +353,83 @@ services.Configure<AbpBundlingOptions>(options =>
});
````
### 外部/CDN文件支持
捆绑系统会自动识别外部/CDN文件,并将其添加到页面中,无需进行任何更改。
#### 在`AbpBundlingOptions`中添加外部/CDN文件
````csharp
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles
.Add("MyStyleBundle", configuration =>
{
configuration
.AddFiles("/styles/my-style1.css")
.AddFiles("/styles/my-style2.css")
.AddFiles("https://cdn.abp.io/bootstrap.css")
.AddFiles("/styles/my-style3.css")
.AddFiles("/styles/my-style4.css");
});
options.ScriptBundles
.Add("MyScriptBundle", configuration =>
{
configuration
.AddFiles("/scripts/my-script1.js")
.AddFiles("/scripts/my-script2.js")
.AddFiles("https://cdn.abp.io/bootstrap.js")
.AddFiles("/scripts/my-script3.js")
.AddFiles("/scripts/my-script4.js");
});
});
````
**输出HTMl:**
````html
<link rel="stylesheet" href="/__bundles/MyStyleBundle.EA8C28419DCA43363E9670973D4C0D15.css?_v=638331889644609730" />
<link rel="stylesheet" href="https://cdn.abp.io/bootstrap.css" />
<link rel="stylesheet" href="/__bundles/MyStyleBundle.AC2E0AA6C461A0949A1295E9BDAC049C.css?_v=638331889644623860" />
<script src="/__bundles/MyScriptBundle.C993366DF8840E08228F3EE685CB08E8.js?_v=638331889644937120"></script>
<script src="https://cdn.abp.io/bootstrap.js"></script>
<script src="/__bundles/MyScriptBundle.2E8D0FDC6334D2A6B847393A801525B7.js?_v=638331889644943970"></script>
````
#### 在TagHelpers中添加外部/CDN文件
````html
<abp-style-bundle name="MyStyleBundle">
<abp-style src="/styles/my-style1.css" />
<abp-style src="/styles/my-style2.css" />
<abp-style src="https://cdn.abp.io/bootstrap.css" />
<abp-style src="/styles/my-style3.css" />
<abp-style src="/styles/my-style4.css" />
</abp-style-bundle>
<abp-script-bundle name="MyScriptBundle">
<abp-script src="/scripts/my-script1.js" />
<abp-script src="/scripts/my-script2.js" />
<abp-script src="https://cdn.abp.io/bootstrap.js" />
<abp-script src="/scripts/my-script3.js" />
<abp-script src="/scripts/my-script4.js" />
</abp-script-bundle>
````
**输出HTMl:**
````html
<link rel="stylesheet" href="/__bundles/MyStyleBundle.C60C7B9C1F539659623BB6E7227A7C45.css?_v=638331889645002500" />
<link rel="stylesheet" href="https://cdn.abp.io/bootstrap.css" />
<link rel="stylesheet" href="/__bundles/MyStyleBundle.464328A06039091534650B0E049904C6.css?_v=638331889645012300" />
<script src="/__bundles/MyScriptBundle.55FDCBF2DCB9E0767AE6FA7487594106.js?_v=638331889645050410"></script>
<script src="https://cdn.abp.io/bootstrap.js"></script>
<script src="/__bundles/MyScriptBundle.191CB68AB4F41C8BF3A7AE422F19A3D2.js?_v=638331889645055490"></script>
````
### 主题
主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md).

1
framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs

@ -17,5 +17,6 @@ public class BlazorGlobalStyleContributor : BundleContributor
context.Files.AddIfNotContains("/_content/Blazorise/blazorise.css");
context.Files.AddIfNotContains("/_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css");
context.Files.AddIfNotContains("/_content/Blazorise.Snackbar/blazorise.snackbar.css");
context.Files.AddIfNotContains("/_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css");
}
}

1
framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs

@ -26,5 +26,6 @@ public class ComponentsComponentsBundleContributor : IBundleContributor
context.Add("_content/Blazorise/blazorise.css");
context.Add("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css");
context.Add("_content/Blazorise.Snackbar/blazorise.snackbar.css");
context.Add("_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css");
}
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs

@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public class BundleConfigurationContext : IBundleConfigurationContext
{
public List<string> Files { get; }
public List<BundleFile> Files { get; }
public IFileProvider FileProvider { get; }
@ -18,7 +18,7 @@ public class BundleConfigurationContext : IBundleConfigurationContext
public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider)
{
Files = new List<string>();
Files = new List<BundleFile>();
ServiceProvider = serviceProvider;
LazyServiceProvider = ServiceProvider.GetRequiredService<IAbpLazyServiceProvider>();
FileProvider = fileProvider;

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs

@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
@ -10,6 +11,18 @@ public static class BundleConfigurationExtensions
return bundleConfiguration;
}
public static BundleConfiguration AddFiles(this BundleConfiguration bundleConfiguration, params BundleFile[] files)
{
bundleConfiguration.Contributors.AddFiles(files);
return bundleConfiguration;
}
public static BundleConfiguration AddExternalFiles(this BundleConfiguration bundleConfiguration, params string[] files)
{
bundleConfiguration.Contributors.AddExternalFiles(files.Select(x => new BundleFile(x, true)).ToArray());
return bundleConfiguration;
}
public static BundleConfiguration AddContributors(this BundleConfiguration bundleConfiguration, params IBundleContributor[] contributors)
{
Check.NotNull(contributors, nameof(contributors));

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs

@ -6,4 +6,14 @@ public static class BundleContributorCollectionExtensions
{
contributors.Add(new BundleFileContributor(files));
}
public static void AddFiles(this BundleContributorCollection contributors, params BundleFile[] files)
{
contributors.Add(new BundleFileContributor(files));
}
public static void AddExternalFiles(this BundleContributorCollection contributors, params BundleFile[] files)
{
contributors.Add(new BundleFileContributor(files));
}
}

31
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs

@ -0,0 +1,31 @@
using System;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public class BundleFile
{
public string FileName { get; set; }
public bool IsExternalFile { get; set; }
public BundleFile(string fileName)
{
FileName = fileName;
IsExternalFile = fileName.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
fileName.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
}
public BundleFile(string fileName, bool isExternalFile)
{
FileName = fileName;
IsExternalFile = isExternalFile;
}
/// <summary>
/// This method is used to compatible with old code.
/// </summary>
public static implicit operator BundleFile(string fileName)
{
return new BundleFile(fileName);
}
}

14
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs

@ -1,22 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public class BundleFileContributor : BundleContributor
{
public string[] Files { get; }
public List<BundleFile> Files { get; }
public BundleFileContributor(params BundleFile[] files)
{
Files = new List<BundleFile>();
Files.AddRange(files);
}
public BundleFileContributor(params string[] files)
{
Files = files ?? Array.Empty<string>();
Files = new List<BundleFile>();
Files.AddRange(files.Select(file => new BundleFile(file)));
}
public override void ConfigureBundle(BundleConfigurationContext context)
{
foreach (var file in Files)
{
context.Files.AddIfNotContains(x => x.Equals(file, StringComparison.OrdinalIgnoreCase), () => file);
context.Files.AddIfNotContains(x => x.FileName.Equals(file.FileName, StringComparison.OrdinalIgnoreCase), () => file);
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs

@ -5,5 +5,5 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public interface IBundleConfigurationContext : IServiceProviderAccessor
{
List<string> Files { get; }
List<BundleFile> Files { get; }
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs

@ -5,11 +5,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public class BundleCacheItem
{
public List<string> Files { get; }
public List<BundleFile> Files { get; }
public List<IDisposable> WatchDisposeHandles { get; }
public BundleCacheItem(List<string> files)
public BundleCacheItem(List<BundleFile> files)
{
Files = files;
WatchDisposeHandles = new List<IDisposable>();

50
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs

@ -12,7 +12,6 @@ using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Scripts;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles;
using Volo.Abp.AspNetCore.Mvc.UI.Resources;
using Volo.Abp.AspNetCore.VirtualFileSystem;
using Volo.Abp.DependencyInjection;
using Volo.Abp.VirtualFileSystem;
@ -56,18 +55,20 @@ public class BundleManager : IBundleManager, ITransientDependency
Logger = NullLogger<BundleManager>.Instance;
}
public virtual async Task<IReadOnlyList<string>> GetStyleBundleFilesAsync(string bundleName)
public virtual async Task<IReadOnlyList<BundleFile>> GetStyleBundleFilesAsync(string bundleName)
{
return await GetBundleFilesAsync(Options.StyleBundles, bundleName, StyleBundler);
}
public virtual async Task<IReadOnlyList<string>> GetScriptBundleFilesAsync(string bundleName)
public virtual async Task<IReadOnlyList<BundleFile>> GetScriptBundleFilesAsync(string bundleName)
{
return await GetBundleFilesAsync(Options.ScriptBundles, bundleName, ScriptBundler);
}
protected virtual async Task<IReadOnlyList<string>> GetBundleFilesAsync(BundleConfigurationCollection bundles, string bundleName, IBundler bundler)
protected virtual async Task<IReadOnlyList<BundleFile>> GetBundleFilesAsync(BundleConfigurationCollection bundles, string bundleName, IBundler bundler)
{
var files = new List<BundleFile>();
var contributors = GetContributors(bundles, bundleName);
var bundleFiles = RequestResources.TryAdd(await GetBundleFilesAsync(contributors));
var dynamicResources = RequestResources.TryAdd(await GetDynamicResourcesAsync(contributors));
@ -77,16 +78,45 @@ public class BundleManager : IBundleManager, ITransientDependency
return bundleFiles.Union(dynamicResources).ToImmutableList();
}
var localBundleFiles = new List<string>();
foreach (var bundleFile in bundleFiles)
{
if (!bundleFile.IsExternalFile)
{
localBundleFiles.Add(bundleFile.FileName);
}
else
{
if (localBundleFiles.Count != 0)
{
files.AddRange(AddToBundleCache(bundleName, bundler, localBundleFiles).Files);
localBundleFiles.Clear();
}
files.Add(bundleFile);
}
}
if (localBundleFiles.Count != 0)
{
files.AddRange(AddToBundleCache(bundleName, bundler, localBundleFiles).Files);
}
return files.Union(dynamicResources).ToImmutableList();
}
private BundleCacheItem AddToBundleCache(string bundleName, IBundler bundler, List<string> bundleFiles)
{
var bundleRelativePath =
Options.BundleFolderName.EnsureEndsWith('/') +
bundleName + "." + bundleFiles.JoinAsString("|").ToMd5() + "." + bundler.FileExtension;
var cacheItem = BundleCache.GetOrAdd(bundleRelativePath, () =>
return BundleCache.GetOrAdd(bundleRelativePath, () =>
{
var cacheValue = new BundleCacheItem(
new List<string>
new List<BundleFile>
{
"/" + bundleRelativePath
new BundleFile("/" + bundleRelativePath)
}
);
@ -104,8 +134,6 @@ public class BundleManager : IBundleManager, ITransientDependency
return cacheValue;
});
return cacheItem.Files.Union(dynamicResources).ToImmutableList();
}
private void WatchChanges(BundleCacheItem cacheValue, List<string> files, string bundleRelativePath)
@ -176,7 +204,7 @@ public class BundleManager : IBundleManager, ITransientDependency
}
}
protected async Task<List<string>> GetBundleFilesAsync(List<IBundleContributor> contributors)
protected async Task<List<BundleFile>> GetBundleFilesAsync(List<IBundleContributor> contributors)
{
var context = CreateBundleConfigurationContext();
@ -198,7 +226,7 @@ public class BundleManager : IBundleManager, ITransientDependency
return context.Files;
}
protected virtual async Task<List<string>> GetDynamicResourcesAsync(List<IBundleContributor> contributors)
protected virtual async Task<List<BundleFile>> GetDynamicResourcesAsync(List<IBundleContributor> contributors)
{
var context = CreateBundleConfigurationContext();

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs

@ -5,7 +5,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling;
public interface IBundleManager
{
Task<IReadOnlyList<string>> GetStyleBundleFilesAsync(string bundleName);
Task<IReadOnlyList<BundleFile>> GetStyleBundleFilesAsync(string bundleName);
Task<IReadOnlyList<string>> GetScriptBundleFilesAsync(string bundleName);
Task<IReadOnlyList<BundleFile>> GetScriptBundleFilesAsync(string bundleName);
}

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs

@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers;
@ -49,7 +50,7 @@ public abstract class AbpBundleItemTagHelper<TTagHelper, TTagHelperService> : Ab
if (Src != null)
{
return new BundleTagHelperFileItem(Src);
return new BundleTagHelperFileItem(new BundleFile(Src));
}
throw new AbpException("abp-script tag helper requires to set either src or type!");

34
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs

@ -3,10 +3,12 @@ using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -63,18 +65,24 @@ public abstract class AbpTagHelperResourceService : ITransientDependency
foreach (var bundleFile in bundleFiles)
{
var file = HostingEnvironment.WebRootFileProvider.GetFileInfo(bundleFile);
if (file == null || !file.Exists)
if (bundleFile.IsExternalFile)
{
Logger.LogError($"Could not find the bundle file '{bundleFile}' for the bundle '{bundleName}'!");
AddErrorScript(viewContext, tagHelper, context, output, bundleFile, bundleName!);
continue;
AddHtmlTag(viewContext, tagHelper, context, output, bundleFile, null);
}
if (file.Length > 0)
else
{
AddHtmlTag(viewContext, tagHelper, context, output, bundleFile + "?_v=" + file.LastModified.UtcTicks);
var file = HostingEnvironment.WebRootFileProvider.GetFileInfo(bundleFile.FileName);
if (file == null || !file.Exists)
{
Logger.LogError($"Could not find the bundle file '{bundleFile.FileName}' for the bundle '{bundleName}'!");
AddErrorScript(viewContext, tagHelper, context, output, bundleFile, bundleName!);
continue;
}
if (file.Length > 0)
{
AddHtmlTag(viewContext, tagHelper, context, output, bundleFile, file);
}
}
}
@ -84,13 +92,13 @@ public abstract class AbpTagHelperResourceService : ITransientDependency
protected abstract void CreateBundle(string bundleName, List<BundleTagHelperItem> bundleItems);
protected abstract Task<IReadOnlyList<string>> GetBundleFilesAsync(string bundleName);
protected abstract Task<IReadOnlyList<BundleFile>> GetBundleFilesAsync(string bundleName);
protected abstract void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file);
protected abstract void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null);
protected virtual void AddErrorScript(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file, string bundleName)
protected virtual void AddErrorScript(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, string bundleName)
{
output.Content.AppendHtml($"<script>console.log(\"%cCould not find the bundle file '{file}' for the bundle '{bundleName}'!\", 'background: yellow; font-size:20px;');</script>{Environment.NewLine}");
output.Content.AppendHtml($"<script>console.log(\"%cCould not find the bundle file '{file.FileName}' for the bundle '{bundleName}'!\", 'background: yellow; font-size:20px;');</script>{Environment.NewLine}");
}
protected virtual string GenerateBundleName(List<BundleTagHelperItem> bundleItems)

14
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
@ -32,12 +33,12 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService
);
}
protected override async Task<IReadOnlyList<string>> GetBundleFilesAsync(string bundleName)
protected override async Task<IReadOnlyList<BundleFile>> GetBundleFilesAsync(string bundleName)
{
return await BundleManager.GetScriptBundleFilesAsync(bundleName);
}
protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file)
protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null)
{
var defer = tagHelper switch
{
@ -46,12 +47,13 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService
_ => false
};
var deferText = (defer || Options.DeferScriptsByDefault || Options.DeferScripts.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
? "defer"
var deferText = (defer || Options.DeferScriptsByDefault || Options.DeferScripts.Any(x => file.FileName.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
? "defer "
: string.Empty;
var nonceText = (viewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString))
? $"nonce=\"{nonceString}\""
? $"nonce=\"{nonceString}\" "
: string.Empty;
output.Content.AppendHtml($"<script {deferText} {nonceText} src=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\"></script>{Environment.NewLine}");
var src = file.IsExternalFile ? file.FileName : viewContext.GetUrlHelper().Content((file.FileName + "?_v=" + fileInfo!.LastModified.UtcTicks).EnsureStartsWith('~'));
output.Content.AppendHtml($"<script {deferText}{nonceText}src=\"{src}\"></script>{Environment.NewLine}");
}
}

16
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Security;
@ -18,7 +19,7 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
public AbpTagHelperStyleService(
IBundleManager bundleManager,
IOptions<AbpBundlingOptions> options,
IWebHostEnvironment hostingEnvironment,
IWebHostEnvironment hostingEnvironment,
IOptions<AbpSecurityHeadersOptions> securityHeadersOptions) : base(
bundleManager,
options,
@ -36,12 +37,12 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
);
}
protected override async Task<IReadOnlyList<string>> GetBundleFilesAsync(string bundleName)
protected override async Task<IReadOnlyList<BundleFile>> GetBundleFilesAsync(string bundleName)
{
return await BundleManager.GetStyleBundleFilesAsync(bundleName);
}
protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file)
protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null)
{
var preload = tagHelper switch
{
@ -50,15 +51,16 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
_ => false
};
if (preload || Options.PreloadStylesByDefault || Options.PreloadStyles.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
var href = file.IsExternalFile ? file.FileName : viewContext.GetUrlHelper().Content((file.FileName + "?_v=" + fileInfo!.LastModified.UtcTicks).EnsureStartsWith('~'));
if (preload || Options.PreloadStylesByDefault || Options.PreloadStyles.Any(x => file.FileName.StartsWith(x, StringComparison.OrdinalIgnoreCase)))
{
output.Content.AppendHtml(SecurityHeadersOptions.UseContentSecurityPolicyScriptNonce
? $"<link rel=\"preload\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" as=\"style\" abp-csp-style />{Environment.NewLine}"
: $"<link rel=\"preload\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" as=\"style\" onload=\"this.rel='stylesheet'\" />{Environment.NewLine}");
? $"<link rel=\"preload\" href=\"{href}\" as=\"style\" abp-csp-style />{Environment.NewLine}"
: $"<link rel=\"preload\" href=\"{href}\" as=\"style\" onload=\"this.rel='stylesheet'\" />{Environment.NewLine}");
}
else
{
output.Content.AppendHtml($"<link rel=\"stylesheet\" href=\"{viewContext.GetUrlHelper().Content(file.EnsureStartsWith('~'))}\" />{Environment.NewLine}");
output.Content.AppendHtml($"<link rel=\"stylesheet\" href=\"{href}\" />{Environment.NewLine}");
}
}
}

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs

@ -5,16 +5,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers;
public class BundleTagHelperFileItem : BundleTagHelperItem
{
[NotNull]
public string File { get; }
public BundleFile File { get; }
public BundleTagHelperFileItem([NotNull] string file)
public BundleTagHelperFileItem([NotNull] BundleFile file)
{
File = Check.NotNull(file, nameof(file));
}
public override string ToString()
{
return File;
return File.FileName;
}
public override void AddToConfiguration(BundleConfiguration configuration)

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs

@ -1,8 +1,9 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Resources;
public interface IWebRequestResources
{
List<string> TryAdd(List<string> resources);
List<BundleFile> TryAdd(List<BundleFile> resources);
}

7
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs

@ -1,23 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.UI.Resources;
public class WebRequestResources : IWebRequestResources, IScopedDependency
{
protected Dictionary<string, List<string>> Resources { get; }
protected Dictionary<string, List<BundleFile>> Resources { get; }
protected IHttpContextAccessor HttpContextAccessor { get; }
public WebRequestResources(IHttpContextAccessor httpContextAccessor)
{
HttpContextAccessor = httpContextAccessor;
Resources = new Dictionary<string, List<string>>();
Resources = new Dictionary<string, List<BundleFile>>();
}
public List<string> TryAdd(List<string> resources)
public List<BundleFile> TryAdd(List<BundleFile> resources)
{
var path = HttpContextAccessor.HttpContext?.Request?.Path ?? "";

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs

@ -30,11 +30,13 @@ public class AbpAspNetCoreMvcUiPackagesModule : AbpModule
//moment
options.AddLanguagesMapOrUpdate(MomentScriptContributor.PackageName,
new NameValue("zh-Hans", "zh-cn"),
new NameValue("zh-Hant", "zh-tw"));
new NameValue("zh-Hant", "zh-tw"),
new NameValue("de-DE", "de"));
options.AddLanguageFilesMapOrUpdate(MomentScriptContributor.PackageName,
new NameValue("zh-Hans", "zh-cn"),
new NameValue("zh-Hant", "zh-tw"));
new NameValue("zh-Hant", "zh-tw"),
new NameValue("de-DE", "de"));
//Timeago
options.AddLanguageFilesMapOrUpdate(TimeagoScriptContributor.PackageName,

22
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs

@ -35,20 +35,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling;
)]
public class SharedThemeGlobalScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddRange(new[]
context.Files.AddRange(new BundleFile[]
{
"/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/widget-manager.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert2/abp-sweetalert2.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js"
"/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/widget-manager.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert2/abp-sweetalert2.js",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js"
});
}
}

8
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs

@ -27,10 +27,10 @@ public class SharedThemeGlobalStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddRange(new[]
context.Files.AddRange(new BundleFile[]
{
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-styles.css",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/date-range-picker/date-range-picker-styles.css"
});
"/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-styles.css",
"/libs/abp/aspnetcore-mvc-ui-theme-shared/date-range-picker/date-range-picker-styles.css"
});
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js

@ -417,7 +417,7 @@
var isUtc = options.isUtc;
dateFormat = dateFormat || options.dateFormat;
if (!date) {
return isUtc ? moment.utc() : moment();
return isUtc ? moment.utc().startOf('day') : moment().startOf('day');
}
if (isUtc) {

60
framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css

@ -0,0 +1,60 @@
.radar-spinner, .radar-spinner * {
box-sizing: border-box;
}
.radar-spinner {
height: 60px;
width: 60px;
position: relative;
}
.radar-spinner .circle {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
animation: radar-spinner-animation 2s infinite;
}
.radar-spinner .circle:nth-child(1) {
padding: calc(60px * 5 * 2 * 0 / 110);
animation-delay: 300ms;
}
.radar-spinner .circle:nth-child(2) {
padding: calc(60px * 5 * 2 * 1 / 110);
animation-delay: 300ms;
}
.radar-spinner .circle:nth-child(3) {
padding: calc(60px * 5 * 2 * 2 / 110);
animation-delay: 300ms;
}
.radar-spinner .circle:nth-child(4) {
padding: calc(60px * 5 * 2 * 3 / 110);
animation-delay: 0ms;
}
.radar-spinner .circle-inner, .radar-spinner .circle-inner-container {
height: 100%;
width: 100%;
border-radius: 50%;
border: calc(60px * 5 / 110) solid transparent;
}
.radar-spinner .circle-inner {
border-left-color: var(--secondary, #ff1d5e);
border-right-color: var(--secondary, #ff1d5e);
}
@keyframes radar-spinner-animation {
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(0deg);
}
}

1
framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj

@ -12,6 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Json\Volo.Abp.Json.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Abstractions\Volo.Abp.MultiTenancy.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>

44
framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs

@ -1,10 +1,14 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using Dapr.Client;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json.SystemTextJson;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Tracing;
namespace Volo.Abp.Dapr;
@ -13,13 +17,21 @@ public class AbpDaprClientFactory : IAbpDaprClientFactory, ISingletonDependency
protected AbpDaprOptions DaprOptions { get; }
protected JsonSerializerOptions JsonSerializerOptions { get; }
protected IDaprApiTokenProvider DaprApiTokenProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; }
protected IOptions<AbpCorrelationIdOptions> AbpCorrelationIdOptions { get; }
public AbpDaprClientFactory(
IOptions<AbpDaprOptions> options,
IOptions<AbpSystemTextJsonSerializerOptions> systemTextJsonSerializerOptions,
IDaprApiTokenProvider daprApiTokenProvider)
IDaprApiTokenProvider daprApiTokenProvider,
ICurrentTenant currentTenant, ICorrelationIdProvider correlationIdProvider,
IOptions<AbpCorrelationIdOptions> abpCorrelationIdOptions)
{
DaprApiTokenProvider = daprApiTokenProvider;
CurrentTenant = currentTenant;
CorrelationIdProvider = correlationIdProvider;
AbpCorrelationIdOptions = abpCorrelationIdOptions;
DaprOptions = options.Value;
JsonSerializerOptions = CreateJsonSerializerOptions(systemTextJsonSerializerOptions.Value);
}
@ -61,11 +73,39 @@ public class AbpDaprClientFactory : IAbpDaprClientFactory, ISingletonDependency
daprEndpoint = DaprOptions.HttpEndpoint;
}
return DaprClient.CreateInvokeHttpClient(
var httpClient = DaprClient.CreateInvokeHttpClient(
appId,
daprEndpoint,
daprApiToken ?? DaprApiTokenProvider.GetDaprApiToken()
);
AddHeaders(httpClient);
return httpClient;
}
protected virtual void AddHeaders(HttpClient httpClient)
{
//CorrelationId
httpClient.DefaultRequestHeaders.Add(AbpCorrelationIdOptions.Value.HttpHeaderName, CorrelationIdProvider.Get());
//TenantId
if (CurrentTenant.Id.HasValue)
{
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
httpClient.DefaultRequestHeaders.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
}
//Culture
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
if (!currentCulture.IsNullOrEmpty())
{
httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture));
}
//X-Requested-With
httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
}
protected virtual JsonSerializerOptions CreateJsonSerializerOptions(AbpSystemTextJsonSerializerOptions systemTextJsonSerializerOptions)

3
framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs

@ -4,10 +4,11 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Dapr;
[DependsOn(typeof(AbpJsonModule))]
[DependsOn(typeof(AbpJsonModule), typeof(AbpMultiTenancyAbstractionsModule))]
public class AbpDaprModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

10
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -161,16 +161,18 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
{
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
var updatedEntity = dbContext.Update(entity).Entity;
if (dbContext.Set<TEntity>().Local.All(e => e != entity))
{
dbContext.Set<TEntity>().Attach(entity);
dbContext.Update(entity);
}
if (autoSave)
{
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return updatedEntity;
return entity;
}
public async override Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)

37
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs

@ -85,8 +85,10 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
case EntityState.Modified:
changeType = IsDeleted(entityEntry) ? EntityChangeType.Deleted : EntityChangeType.Updated;
break;
case EntityState.Detached:
case EntityState.Unchanged:
changeType = EntityChangeType.Updated; // Navigation property changes.
break;
case EntityState.Detached:
default:
return null;
}
@ -184,6 +186,21 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
}
}
if (entityEntry.State == EntityState.Unchanged)
{
foreach (var navigation in entityEntry.Navigations)
{
if (navigation.IsModified || (navigation is ReferenceEntry && navigation.As<ReferenceEntry>().TargetEntry?.State == EntityState.Modified))
{
propertyChanges.Add(new EntityPropertyChangeInfo
{
PropertyName = navigation.Metadata.Name,
PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!
});
}
}
}
return propertyChanges;
}
@ -205,12 +222,26 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
protected virtual bool ShouldSaveEntityHistory(EntityEntry entityEntry, bool defaultValue = false)
{
if (entityEntry.State == EntityState.Detached ||
entityEntry.State == EntityState.Unchanged)
if (entityEntry.State == EntityState.Detached)
{
return false;
}
if (entityEntry.State == EntityState.Unchanged)
{
if (entityEntry.Navigations.Any(navigationEntry => navigationEntry.IsModified))
{
return true;
}
if (entityEntry.Navigations.Where(x => x is ReferenceEntry).Cast<ReferenceEntry>().Any(x => x.TargetEntry != null && x.TargetEntry.State == EntityState.Modified))
{
return true;
}
return false;
}
var entityType = entityEntry.Metadata.ClrType;
if (!EntityHelper.IsEntity(entityType) && !EntityHelper.IsValueObject(entityType))

6
framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs

@ -2,12 +2,6 @@ namespace Volo.Abp.UI.Navigation;
public static class StandardMenus
{
/* TODO: Consider to create nested class like
* StandardMenus.Application.Main
* StandardMenus.Application.User
* StandardMenus.Application.Shortcut
*/
public const string Main = "Main";
public const string User = "User";
public const string Shortcut = "Shortcut";

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "سيتم حذف {0}!",
"ItemWillBeDeletedMessage": "سوف يتم حذف هذا البند!",
"ManageYourAccount": "إدارة حسابك",
"OthersGroup": "آخرون"
"OthersGroup": "آخرون",
"Today": "اليوم",
"Apply": "يتقدم"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} bude smazáno!",
"ItemWillBeDeletedMessage": "Tato položka bude smazána!",
"ManageYourAccount": "Spravujte svůj účet",
"OthersGroup": "Jiný"
"OthersGroup": "Jiný",
"Today": "Dnes",
"Apply": "Aplikovat"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} wird gelöscht!",
"ItemWillBeDeletedMessage": "Dieses Element wird gelöscht!",
"ManageYourAccount": "Verwalten Sie Ihr Benutzerkonto",
"OthersGroup":"Andere"
"OthersGroup":"Andere",
"Today": "Heute",
"Apply": "Anwenden"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "Το {0} θα διαγραφεί",
"ItemWillBeDeletedMessage": "Αυτό το στοιχείο θα διαγραφεί!",
"ManageYourAccount": "Διαχείριση Λογαριασμού",
"OthersGroup":"άλλος"
"OthersGroup":"άλλος",
"Today": "Σήμερα",
"Apply": "Ισχύουν"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json

@ -50,6 +50,8 @@
"ItemWillBeDeletedMessage": "This item will be deleted!",
"ManageYourAccount": "Manage your account",
"OthersGroup": "Other",
"NotAssigned": "Not Assigned"
"NotAssigned": "Not Assigned",
"Today": "Today",
"Apply": "Apply"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} serán borrados!",
"ItemWillBeDeletedMessage": "Este elemento será borrado",
"ManageYourAccount": "Administrar cuenta",
"OthersGroup": "Otra"
"OthersGroup": "Otra",
"Today": "Hoy",
"Apply": "Aplicar"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} حذف خواهد شد!",
"ItemWillBeDeletedMessage": "این مورد حذف خواهد شد!",
"ManageYourAccount": "حساب خود را مدیریت کنید",
"OthersGroup": "دیگر"
"OthersGroup": "دیگر",
"Today": "امروز",
"Apply": "درخواست دادن"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} poistetaan!",
"ItemWillBeDeletedMessage": "Tämä kohde poistetaan!",
"ManageYourAccount": "Hallitse tiliäsi",
"OthersGroup": "Muut"
"OthersGroup": "Muut",
"Today": "Tänään",
"Apply": "Käytä"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!",
"ItemWillBeDeletedMessage": "Cet objet va être supprimé!",
"ManageYourAccount": "Gérer votre compte",
"OthersGroup": "Autre"
"OthersGroup": "Autre",
"Today": "Aujourd'hui",
"Apply": "Appliquer"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} हटा दिया जाएगा!",
"ItemWillBeDeletedMessage": "यह आइटम हटा दिया जाएगा!",
"ManageYourAccount": "अपने खाते का प्रबंधन",
"OthersGroup": "अन्य"
"OthersGroup": "अन्य",
"Today": "आज",
"Apply": "आवेदन करना"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} zapis će biti obrisan!",
"ItemWillBeDeletedMessage": "Ovaj zapis će biti obrisan!",
"ManageYourAccount": "Upravljaj korisničkim računom",
"OthersGroup": "Drugi"
"OthersGroup": "Drugi",
"Today": "Danas",
"Apply": "primijeniti"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!",
"ItemWillBeDeletedMessage": "Ez az elem törlődik!",
"ManageYourAccount": "Kezelje fiókját",
"OthersGroup": "Egyéb"
"OthersGroup": "Egyéb",
"Today": "Ma",
"Apply": "Alkalmaz"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} verður eytt!",
"ItemWillBeDeletedMessage": "Þessum lið verður eytt!",
"ManageYourAccount": "Stillingar notandaaðgangs",
"OthersGroup": "Annað"
"OthersGroup": "Annað",
"Today": "Í dag",
"Apply": "Sækja um"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} sarà eliminato!",
"ItemWillBeDeletedMessage": "Questo elemento sarà eliminato!",
"ManageYourAccount": "Gestisci il tuo account",
"OthersGroup": "Altra"
"OthersGroup": "Altra",
"Today": "Oggi",
"Apply": "Fare domanda a"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} wordt verwijderd!",
"ItemWillBeDeletedMessage": "Dit item wordt verwijderd!",
"ManageYourAccount": "Beheer uw account",
"OthersGroup": "Ander"
"OthersGroup": "Ander",
"Today": "Vandaag",
"Apply": "Toepassen"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} zostanie usunięty!",
"ItemWillBeDeletedMessage": "Ten element zostanie usunięty!",
"ManageYourAccount": "Zarządzaj kontem",
"OthersGroup": "Inny"
"OthersGroup": "Inny",
"Today": "Dzisiaj",
"Apply": "Stosować"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} será excluído!",
"ItemWillBeDeletedMessage": "Este item será excluído!",
"ManageYourAccount": "Gerenciar sua conta",
"OthersGroup": "Outra"
"OthersGroup": "Outra",
"Today": "Hoje",
"Apply": "Aplicar"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} va fi şters!",
"ItemWillBeDeletedMessage": "Acest articol va fi şters!",
"ManageYourAccount": "Administraţi-vă contul",
"OthersGroup": "Alte"
"OthersGroup": "Alte",
"Today": "Astăzi",
"Apply": "aplica"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} будет удален!",
"ItemWillBeDeletedMessage": "Этот предмет будет удален!",
"ManageYourAccount": "Настройте свой аккаунт",
"OthersGroup": "Другой"
"OthersGroup": "Другой",
"Today": "Сегодня",
"Apply": "Применять"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} sa vymaže!",
"ItemWillBeDeletedMessage": "Táto položka bude vymazaná!",
"ManageYourAccount": "Spravovať svoje konto",
"OthersGroup": "Iné"
"OthersGroup": "Iné",
"Today": "Dnes",
"Apply": "Použiť"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} bo izbrisan!",
"ItemWillBeDeletedMessage": "Ta element bo izbrisan!",
"ManageYourAccount": "Upravljajte svoj račun",
"OthersGroup": "Ostalo"
"OthersGroup": "Ostalo",
"Today": "Danes",
"Apply": "Prijavite se"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json

@ -49,6 +49,8 @@
"ItemWillBeDeletedMessageWithFormat": "{0} sẽ bị xóa!",
"ItemWillBeDeletedMessage": "Vật phẩm này sẽ bị xoá!",
"ManageYourAccount": "Quản lý tài khoản của bạn",
"OthersGroup": "Khác"
"OthersGroup": "Khác",
"Today": "Hôm nay",
"Apply": "Áp dụng"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json

@ -51,6 +51,8 @@
"ManageYourAccount": "管理你的账户",
"OthersGroup": "其他",
"CopiedToTheClipboard": "已复制到剪贴板",
"NotAssigned": "未分配"
"NotAssigned": "未分配",
"Today": "今天",
"Apply": "应用"
}
}

4
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json

@ -51,6 +51,8 @@
"ManageYourAccount": "管理個人帳號",
"OthersGroup": "其他",
"CopiedToTheClipboard": "已复制到剪贴板",
"NotAssigned": "未分配"
"NotAssigned": "未分配",
"Today": "今天",
"Apply": "應用"
}
}

51
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities;
namespace Volo.Abp.Auditing.App.Entities;
[Audited]
public class AppEntityWithNavigations : AggregateRoot<Guid>
{
protected AppEntityWithNavigations()
{
}
public AppEntityWithNavigations(Guid id, string name)
: base(id)
{
Name = name;
FullName = name;
}
public string Name { get; set; }
public string FullName { get; set; }
public virtual AppEntityWithNavigationChildOneToOne OneToOne { get; set; }
public virtual List<AppEntityWithNavigationChildOneToMany> OneToMany { get; set; }
public virtual List<AppEntityWithNavigationChildManyToMany> ManyToMany { get; set; }
}
[Audited]
public class AppEntityWithNavigationChildOneToOne : Entity<Guid>
{
public string ChildName { get; set; }
}
[Audited]
public class AppEntityWithNavigationChildOneToMany : Entity<Guid>
{
public Guid AppEntityWithNavigationId { get; set; }
public string ChildName { get; set; }
}
[Audited]
public class AppEntityWithNavigationChildManyToMany : Entity<Guid>
{
public string ChildName { get; set; }
}

11
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs

@ -27,6 +27,8 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
public DbSet<AppEntityWithValueObject> AppEntityWithValueObject { get; set; }
public DbSet<AppEntityWithNavigations> AppEntityWithNavigations { get; set; }
public AbpAuditingTestDbContext(DbContextOptions<AbpAuditingTestDbContext> options)
: base(options)
{
@ -42,5 +44,14 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
b.ConfigureByConvention();
b.OwnsOne(v => v.AppEntityWithValueObjectAddress);
});
modelBuilder.Entity<AppEntityWithNavigations>(b =>
{
b.ConfigureByConvention();
b.HasOne(x => x.OneToOne).WithOne().HasForeignKey<AppEntityWithNavigationChildOneToOne>(x => x.Id);
b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId);
b.HasMany(x => x.ManyToMany).WithMany();
});
}
}

168
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs

@ -411,14 +411,172 @@ public class Auditing_Tests : AbpAuditingTestBase
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 1 &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" &&
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName &&
x.EntityChanges[1].PropertyChanges.Count == 1 &&
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress)));
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.AppEntityWithValueObjectAddress = null;
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 1 &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" &&
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName));
#pragma warning restore 4014
}
[Fact]
public virtual async Task Should_Write_AuditLog_For_Navigations_Changes()
{
var entityId = Guid.NewGuid();
var repository = ServiceProvider.GetRequiredService<IBasicRepository<AppEntityWithNavigations, Guid>>();
await repository.InsertAsync(new AppEntityWithNavigations(entityId, "test name"));
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.FullName = "test full name";
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 1 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 1 &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"test name\"" &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "\"test full name\"" &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.FullName) &&
x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName));
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.OneToOne = new AppEntityWithNavigationChildOneToOne
{
ChildName = "ChildName"
};
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName &&
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
x.EntityChanges[1].PropertyChanges.Count == 1 &&
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) &&
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName));
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.OneToMany = new List<AppEntityWithNavigationChildOneToMany>()
{
new AppEntityWithNavigationChildOneToMany
{
AppEntityWithNavigationId = entity.Id,
ChildName = "ChildName1"
}
};
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName &&
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
x.EntityChanges[1].PropertyChanges.Count == 1 &&
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) &&
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName));
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.ManyToMany = new List<AppEntityWithNavigationChildManyToMany>()
{
new AppEntityWithNavigationChildManyToMany
{
ChildName = "ChildName1"
}
};
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName &&
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName &&
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
x.EntityChanges[1].PropertyChanges.Count == 1 &&
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) &&
x.EntityChanges[1].PropertyChanges[0].OriginalValue == "\"England\"" &&
x.EntityChanges[1].PropertyChanges[0].NewValue == "\"Germany\""));
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) &&
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildManyToMany>).FullName));
#pragma warning restore 4014
}

2
latest-versions.json

@ -1,6 +1,6 @@
[
{
"version": "7.4.0",
"version": "7.4.1",
"releaseDate": "",
"type": "stable",
"message": ""

43
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs

@ -242,14 +242,16 @@ public class LoginModel : AccountPageModel
var user = await UserManager.FindByEmailAsync(email);
if (user == null)
{
user = await CreateExternalUserAsync(loginInfo);
return RedirectToPage("./Register", new {
IsExternalLogin = true,
ExternalLoginAuthSchema = loginInfo.LoginProvider,
ReturnUrl = returnUrl
});
}
else
if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null)
{
if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null)
{
CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo));
}
CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo));
}
await SignInManager.SignInAsync(user, false);
@ -264,35 +266,6 @@ public class LoginModel : AccountPageModel
return RedirectSafely(returnUrl, returnUrlHash);
}
protected virtual async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email) ?? info.Principal.FindFirstValue(ClaimTypes.Email);
var userName = await GetUserNameFromEmail(emailAddress);
var user = new IdentityUser(GuidGenerator.Create(), userName, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
user.Name = info.Principal.FindFirstValue(AbpClaimTypes.Name);
user.Surname = info.Principal.FindFirstValue(AbpClaimTypes.SurName);
var phoneNumber = info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumber);
if (!phoneNumber.IsNullOrWhiteSpace())
{
var phoneNumberConfirmed = string.Equals(info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase);
user.SetPhoneNumber(phoneNumber, phoneNumberConfirmed);
}
await UserManager.UpdateAsync(user);
return user;
}
protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds()
{
if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress))

2
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml

@ -12,7 +12,7 @@
<a href="@Url.Page("./Login", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Login"]</a>
</strong>
<form method="post" class="mt-4">
@if ((!Model.IsExternalLogin || Model.UserNameExtracted) && Model.EnableLocalRegister)
@if (Model.EnableLocalRegister || Model.IsExternalLogin)
{
<abp-input asp-for="Input.UserName" auto-focus="true"/>
}

2
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -37,7 +37,6 @@ public class RegisterModel : AccountPageModel
[BindProperty(SupportsGet = true)]
public string ExternalLoginAuthSchema { get; set; }
public bool UserNameExtracted { get; set; }
public IEnumerable<ExternalProviderModel> ExternalProviders { get; set; }
public IEnumerable<ExternalProviderModel> VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName));
public bool EnableLocalRegister { get; set; }
@ -128,7 +127,6 @@ public class RegisterModel : AccountPageModel
{
Input.UserName = await GetUserNameFromEmail(Input.EmailAddress);
}
UserNameExtracted = true;
await RegisterExternalUserAsync(externalLoginInfo, Input.UserName, Input.EmailAddress);
}
else

25
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html

@ -24,23 +24,24 @@
<ng-container *ngFor="let prop of propList; let i = index; trackBy: trackByFn">
<ngx-datatable-column
*abpVisible="prop.columnVisible(getInjected)"
[width]="columnWidths[i + 1] || 200"
[name]="prop.displayName | abpLocalization "
[name]="prop.displayName | abpLocalization"
[prop]="prop.name"
[sortable]="prop.sortable"
>
<ng-template ngx-datatable-header-template let-column="column" >
<span
*ngIf="prop.tooltip; else elseBlock"
[ngbTooltip]="prop.tooltip | abpLocalization"
container="body"
>
{{column.name}} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
<ng-template #elseBlock>
{{column.name}}
<ng-template ngx-datatable-header-template let-column="column">
<span
*ngIf="prop.tooltip; else elseBlock"
[ngbTooltip]="prop.tooltip | abpLocalization"
container="body"
>
{{ column.name }} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
<ng-template #elseBlock>
{{ column.name }}
</ng-template>
</ng-template>
</ng-template>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *ngIf="row['_' + prop.name]?.visible">

13
npm/ng-packs/packages/components/extensible/src/lib/models/entity-props.ts

@ -1,5 +1,5 @@
import { ABP, escapeHtmlChars } from '@abp/ng.core';
import { Type } from '@angular/core';
import { InjectFlags, InjectOptions, InjectionToken, Type } from '@angular/core';
import { Observable, of } from 'rxjs';
import { O } from 'ts-toolbelt';
import { ActionCallback } from './actions';
@ -31,6 +31,7 @@ export class EntityProp<R = any> extends Prop<R> {
readonly component?: Type<any>;
readonly enumList?: Array<ABP.Option<any>>;
readonly tooltip?: string;
readonly columnVisible: ColumnPredicate;
constructor(options: EntityPropOptions<R>) {
super(
@ -41,7 +42,8 @@ export class EntityProp<R = any> extends Prop<R> {
options.visible,
options.isExtra,
);
this.columnVisible = options.columnVisible || (() => true);
this.columnWidth = options.columnWidth;
this.sortable = options.sortable || false;
this.valueResolver =
@ -72,6 +74,7 @@ export type EntityPropOptions<R = any> = O.Optional<
O.Writable<EntityProp<R>>,
| 'permission'
| 'visible'
| 'columnVisible'
| 'displayName'
| 'isExtra'
| 'columnWidth'
@ -85,4 +88,10 @@ export type EntityPropOptions<R = any> = O.Optional<
export type EntityPropDefaults<R = any> = Record<string, EntityProp<R>[]>;
export type EntityPropContributorCallback<R = any> = PropContributorCallback<EntityPropList<R>>;
export type EntityPropContributorCallbacks<R = any> = PropContributorCallbacks<EntityPropList<R>>;
export type ColumnPredicate = (getInjected: GetInjected, auxData?: any) => boolean;
export type GetInjected = <T>(
token: Type<T> | InjectionToken<T>,
notFoundValue?: T,
options?: InjectOptions | InjectFlags,
) => T;
type PropDataObject = { [key: string]: any };

4
npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts

@ -19,8 +19,8 @@ export class HttpErrorReporterService {
return this._errors$.value;
}
reportError = (error: HttpErrorResponse) => {
reportError(error: HttpErrorResponse) {
this._reporter$.next(error);
this._errors$.next([...this.errors, error]);
};
}
}

117
npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts

@ -1,15 +1,26 @@
import { APP_BASE_HREF } from '@angular/common';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { provideRouter, Route, Router, RouterModule } from '@angular/router';
import {
createServiceFactory,
createSpyObject,
SpectatorService,
SpyObject,
} from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { PermissionGuard } from '../guards/permission.guard';
import { permissionGuard, PermissionGuard } from '../guards/permission.guard';
import { HttpErrorReporterService } from '../services/http-error-reporter.service';
import { PermissionService } from '../services/permission.service';
import { RoutesService } from '../services/routes.service';
import { CORE_OPTIONS } from '../tokens/options.token';
import { IncludeLocalizationResourcesProvider } from '../providers';
import { TestBed } from '@angular/core/testing';
import { RouterTestingHarness } from '@angular/router/testing';
import { OTHERS_GROUP } from '../tokens';
import { SORT_COMPARE_FUNC, compareFuncFactory } from '../tokens/compare-func.token';
import { CoreModule } from '../core.module';
import { AuthService } from '../abstracts';
describe('PermissionGuard', () => {
let spectator: SpectatorService<PermissionGuard>;
@ -18,6 +29,10 @@ describe('PermissionGuard', () => {
let httpErrorReporter: SpyObject<HttpErrorReporterService>;
let permissionService: SpyObject<PermissionService>;
const mockOAuthService = {
isAuthenticated: true,
};
@Component({ template: '' })
class DummyComponent {}
@ -27,25 +42,25 @@ describe('PermissionGuard', () => {
declarations: [DummyComponent],
imports: [
HttpClientTestingModule,
RouterModule.forRoot(
[
{
path: 'test',
component: DummyComponent,
data: {
requiredPolicy: 'TestPolicy',
},
RouterModule.forRoot([
{
path: 'test',
component: DummyComponent,
data: {
requiredPolicy: 'TestPolicy',
},
],
{},
),
},
]),
],
providers: [
{
provide: APP_BASE_HREF,
useValue: '/',
},
{ provide: AuthService, useValue: mockOAuthService },
{ provide: CORE_OPTIONS, useValue: { skipGetAppConfiguration: true } },
{ provide: OTHERS_GROUP, useValue: 'AbpUi::OthersGroup' },
{ provide: SORT_COMPARE_FUNC, useValue: compareFuncFactory },
IncludeLocalizationResourcesProvider,
],
});
@ -108,3 +123,77 @@ describe('PermissionGuard', () => {
});
});
});
@Component({ standalone: true, template: '' })
class DummyComponent {}
describe('authGuard', () => {
let permissionService: SpyObject<PermissionService>;
let httpErrorReporter: SpyObject<HttpErrorReporterService>;
const mockOAuthService = {
isAuthenticated: true,
};
const routes: Route[] = [
{
path: 'dummy',
component: DummyComponent,
canActivate: [permissionGuard],
data: {
requiredPolicy: 'TestPolicy',
},
},
{
path: 'zibzib',
component: DummyComponent,
canActivate: [permissionGuard],
},
];
beforeEach(() => {
httpErrorReporter = createSpyObject(HttpErrorReporterService);
permissionService = createSpyObject(PermissionService);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, CoreModule.forRoot()],
providers: [
{ provide: AuthService, useValue: mockOAuthService },
{ provide: PermissionService, useValue: permissionService },
{ provide: HttpErrorReporterService, useValue: httpErrorReporter },
provideRouter(routes),
],
});
});
it('should return true when the grantedPolicy is true', async () => {
permissionService.getGrantedPolicy$.andReturn(of(true));
await RouterTestingHarness.create('/dummy');
expect(TestBed.inject(Router).url).toEqual('/dummy');
expect(httpErrorReporter.reportError).not.toHaveBeenCalled();
});
it('should return false and report an error when the grantedPolicy is false', async () => {
permissionService.getGrantedPolicy$.andReturn(of(false));
await RouterTestingHarness.create('/dummy');
expect(TestBed.inject(Router).url).not.toEqual('/dummy');
expect(httpErrorReporter.reportError).toHaveBeenCalled();
expect(httpErrorReporter.reportError).toBeCalledWith({ status: 403 });
});
it('should check the requiredPolicy from RoutesService', async () => {
permissionService.getGrantedPolicy$.mockImplementation(policy => {
return of(policy === 'TestPolicy');
});
await RouterTestingHarness.create('/dummy');
expect(TestBed.inject(Router).url).toEqual('/dummy');
expect(httpErrorReporter.reportError).not.toHaveBeenCalled();
});
it('should return Observable<true> if RoutesService does not have requiredPolicy for given URL', async () => {
await RouterTestingHarness.create('/zibzib');
expect(TestBed.inject(Router).url).toEqual('/zibzib');
});
});

65
npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts

@ -1,14 +1,25 @@
import { createServiceFactory, SpectatorService, createSpyObject } from '@ngneat/spectator/jest';
import { OAuthService } from 'angular-oauth2-oidc';
import { AbpOAuthGuard } from '../guards/oauth.guard';
import { AbpOAuthGuard, abpOAuthGuard } from '../guards/oauth.guard';
import { AuthService } from '@abp/ng.core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import {
ActivatedRouteSnapshot,
Route,
Router,
RouterStateSnapshot,
provideRouter,
} from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { RouterTestingHarness } from '@angular/router/testing';
import { SpyObject } from '@ngneat/spectator';
describe('AuthGuard', () => {
let spectator: SpectatorService<AbpOAuthGuard>;
let guard : AbpOAuthGuard;
const route = createSpyObject<ActivatedRouteSnapshot>(ActivatedRouteSnapshot)
const state = createSpyObject<RouterStateSnapshot>(RouterStateSnapshot)
let guard: AbpOAuthGuard;
const route = createSpyObject<ActivatedRouteSnapshot>(ActivatedRouteSnapshot);
const state = createSpyObject<RouterStateSnapshot>(RouterStateSnapshot);
const createService = createServiceFactory({
service: AbpOAuthGuard,
@ -34,3 +45,47 @@ describe('AuthGuard', () => {
expect(navigateToLoginSpy).toHaveBeenCalled();
});
});
@Component({ standalone: true, template: '' })
class DummyComponent {}
describe('authGuard', () => {
let oAuthService: SpyObject<OAuthService>;
let authService: SpyObject<AuthService>;
const routes: Route[] = [
{
path: 'dummy',
canActivate: [abpOAuthGuard],
component: DummyComponent,
},
];
beforeEach(() => {
authService = createSpyObject(AuthService);
oAuthService = createSpyObject(OAuthService);
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: AuthService, useValue: authService },
{ provide: OAuthService, useValue: oAuthService },
provideRouter(routes),
],
});
});
it('should move to the dummy route', async () => {
oAuthService.hasValidAccessToken.andReturn(true);
await RouterTestingHarness.create('/dummy');
expect(TestBed.inject(Router).url).toEqual('/dummy');
});
it("should'nt move to the dummy route", async () => {
oAuthService.hasValidAccessToken.andReturn(false);
await RouterTestingHarness.create('/dummy');
expect(authService.navigateToLogin).toHaveBeenCalled();
expect(TestBed.inject(Router).url).not.toEqual('/dummy');
expect(TestBed.inject(Router).url).toEqual('/');
});
});

4
npm/ng-packs/packages/schematics/src/index.ts

@ -1 +1,3 @@
export {};
export * from './enums';
export * from './models';
export * from './constants';

Loading…
Cancel
Save