Browse Source

Merge branch 'dev' into dynamic-claims-middleware

pull/18064/head
maliming 3 years ago
parent
commit
aed0551015
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  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. 138
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md
  6. BIN
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png
  7. BIN
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png
  8. 100
      docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md
  9. 89
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md
  10. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png
  11. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png
  12. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png
  13. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png
  14. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png
  15. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png
  16. 79
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md
  17. BIN
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png
  18. 75
      docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md
  19. 4
      docs/en/Startup-Templates/Application.md
  20. 2
      docs/en/Timing.md
  21. 8
      docs/en/Tutorials/Part-5.md
  22. 27
      docs/en/UI/Angular/Data-Table-Column-Extensions.md
  23. 11
      docs/en/UI/Angular/Entity-Action-Extensions.md
  24. 1
      docs/en/UI/Angular/OAuth-Module.md
  25. 8
      docs/en/UI/Angular/Permission-Management.md
  26. 77
      docs/en/UI/AspNetCore/Bundling-Minification.md
  27. 4
      docs/zh-Hans/Startup-Templates/Application.md
  28. 4
      docs/zh-Hans/Timing.md
  29. 8
      docs/zh-Hans/Tutorials/Part-5.md
  30. 10
      docs/zh-Hans/UI/Angular/Permission-Management.md
  31. 77
      docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md
  32. 1
      framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs
  33. 1
      framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs
  34. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs
  35. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs
  36. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs
  37. 31
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs
  38. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs
  39. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs
  40. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs
  41. 50
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs
  42. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs
  43. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs
  44. 34
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs
  45. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
  46. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
  47. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs
  48. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs
  49. 7
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs
  50. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs
  51. 22
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs
  52. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs
  53. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
  54. 60
      framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css
  55. 1
      framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj
  56. 44
      framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs
  57. 3
      framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs
  58. 10
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  59. 37
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs
  60. 6
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs
  61. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  62. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  63. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  64. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  65. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  66. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  67. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  68. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  69. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  70. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  71. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  72. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  73. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  74. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  75. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  76. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  77. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  78. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  79. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json
  80. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json
  81. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json
  82. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json
  83. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json
  84. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  85. 51
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs
  86. 11
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs
  87. 168
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs
  88. 2
      latest-versions.json
  89. 43
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  90. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml
  91. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  92. 4
      npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts
  93. 117
      npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts
  94. 65
      npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts
  95. 4
      npm/ng-packs/packages/schematics/src/index.ts
  96. 25
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html
  97. 13
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.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

138
docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md

@ -0,0 +1,138 @@
# ASP.NET 8: What's New About Authentication and Authorization
In ASP.NET 8, the concept of authentication and authorization is undergoing a transformation. Specifically, ASP.NET Core Identity is transitioning from a focus on traditional web pages to a more API-driven approach. We will see what the Identity API endpoints are, why we need them, and how to use them in detail. So, let's dive in.
## What is ASP.NET Core Identity?
ASP.NET Core Identity is like an extra toolkit that comes with ASP.NET Core. It offers a bunch of services to help you deal with user accounts in your ASP.NET Core application. These services include both basic concepts and ready-made solutions for each task.
With ASP.NET Core Identity, you can save user accounts in your app, handle user information, add extra security with two-factor authentication, and even connect other log in options, like social media logins, to user accounts. In simple terms, it's all about keeping user accounts in your app and making it easy for users to log in.
![](./identity_endpoints_scaffold.png)
Prior to ASP.NET 8, there were limitations on using ASP.NET Identity in Single Page Applications (SPAs). Because the ASP.NET Core Identity framework is primarily tailored for traditional server-rendered web applications, like ASP.NET Core MVC or Razor Pages apps. However, the Identity API endpoints that come with ASP.NET 8 aim to solve token-based authentication and authorization in SPA without external dependencies.
## Why do we need Identity APIs?
The introduction of Identity endpoints in ASP.NET 8 aims to streamline the process of integrating ASP.NET Core Identity into both API server applications and front-end SPAs like those built with JavaScript or Blazor. To understand the importance of these endpoints, let's first examine the disadvantages of using ASP.NET Core Identity before ASP.NET 8.
Let's delve into a common scenario:
Imagine you have a straightforward application composed of an ASP.NET Core backend that exposes APIs. On the client side, you have a SPA application that communicates with these APIs. Now, you want to incorporate user accounts, complete with authentication and authorization, into your application.
Before the advent of the identity endpoints, you could add ASP.NET Core Identity and the default Razor Pages UI to your app by adding a few packages, updating your database schema, and registering some services. However, this approach had some disadvantages. From a user experience perspective, the full-page refreshes of Razor Pages can be a major drawback especially compared to the fluidity of SPAs. On the other hand, from a developer's perspective, if you want the default pages to be compatible with the rest of your application, you may need to update more than 30 pages. Moreover, the default Razor Pages UI uses traditional cookie-based authentication, not bearer tokens.
In ASP.NET 8, identity endpoints were introduced to simplify the process of adding user accounts to ASP.NET Core API apps used with SPAs or mobile. These endpoints streamline the user management process by providing an alternative to the traditional Razor Page Identity UI pages. Additionally, a new endpoint is included for retrieving bearer tokens, which can be used for authentication. These changes directly address the concerns mentioned earlier, making it easier to create a user-friendly and integrated experience within your app, with no full-page refreshes or styling conflicts.
## How to add Identity APIs?
In this part, I'll create a demo application, add all of the required packages, and lastly map the Identity APIs.
Create a new ASP.NET Core app with the following command and change the directory to the created application folder:
```bash
dotnet new webapi --name NewIdentityEndpoints -o IdentityDemo
cd IdentityDemo
```
After that, open the application in your favorite IDE and add the required packages:
```csharp
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0-rc.2.23480.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0-rc.2.23480.1" />
```
**Note:** I used the `.NET 8.0.100-rc.2-**` SDK for everything in this post.
Then replace `Program.cs` as below:
```csharp
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme);
builder.Services.AddAuthorizationBuilder();
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase("AppDb"));
builder.Services.AddIdentityCore<MyCustomUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddApiEndpoints();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapGroup("/my-identity-api").MapIdentityApi<IdentityUser>();
app.MapGet("/", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.Run();
class MyCustomUser : IdentityUser { }
class ApplicationDbContext : IdentityDbContext<MyCustomUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
}
```
When you run the application, you will see a result as the following:
![](./new-identity-endpoints.png)
## Custom Authorization Policies
Prior to ASP.NET 8, adding a parameterized authorization policy to an endpoint required writing a lot of code.
- Implementing an `AuthorizeAttribute` for each policy
- Implementing an `AuthorizationPolicyProvider` to process a custom policy from a string-based contract
- Implementing an `AuthorizationRequirement` for the policy
- Implementing an `AuthorizationHandler` for each requirement
However, implementing the `IAuthorizationRequirementData` interface that comes with ASP.NET 8, your application code should look like this:
```csharp
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData
{
public MinimumAgeAuthorizeAttribute(int age) => Age =age;
public int Age { get; }
public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
yield return this;
}
}
class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement)
{
...
}
}
```
See [here](https://gist.github.com/captainsafia/7c54e92d12df695ff0908e989fb8531f) for the complete code.
## Conclusion
In this article, I've shown you the Identity APIs introduced with ASP.NET 8, why we need them, and how we can add them to our application, and finally I explained how to define a custom authorization policy with fewer lines of code.
## References
- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#authentication-and-authorization
- https://auth0.com/blog/whats-new-dotnet8-authentication-authorization/
- https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/
- https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/

BIN
docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

100
docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md

@ -0,0 +1,100 @@
# .NET 8: Serialization Improvements
The [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) namespace provides functionality for serializing to and deserializing from JSON. In .NET 8, there have been many improvements to the System.Text.Json serialization and deserialization functionality.
## Source generator
.NET 8 includes enhancements of the System.Text.Json source generator that are aimed at making the Native AOT experience on par with the reflection-based serializer. This is important because now you can select the source generator for System.Text.Json. To see a comparison of Reflection versus source generation in System.Text.Json, check out the comparison [documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation?pivots=dotnet-8-0#reflection).
## Interface hierarchies
.NET 8 adds support for serializing properties from interface hierarchies. Here is a code sample that shows this feature.
```csharp
ICar car = new MyCar { Color = "Red", NumberOfWheels = 4 };
JsonSerializer.Serialize(car); // {"Color":"Red","NumberOfWheels":4}
public interface IVehicle
{
public string Color { get; set; }
}
public interface ICar : IVehicle
{
public int NumberOfWheels { get; set; }
}
public class MyCar : ICar
{
public string Color { get; set; }
public int NumberOfWheels { get; set; }
}
```
## Naming policies
Two new naming policies `snake_case` and `kebab-case` have been added. You can use them as shown below:
```csharp
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
JsonSerializer.Serialize(new { CreationTime = new DateTime(2023,11,6) }, options); // {"creation_time":"2023-11-06T00:00:00"}
```
## Read-only properties
With .NET 8, you can deserialize onto read-only fields or properties. This feature can be globally enabled by setting `PreferredObjectCreationHandling` to `JsonObjectCreationHandling.Populate`. You can also enable this feature for a class or one of its members by adding the `[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]` attribute. Here is a sample:
```csharp
using System.Text.Json;
using System.Text.Json.Serialization;
var book = JsonSerializer.Deserialize<Book>("""{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}}""")!;
Console.WriteLine(JsonSerializer.Serialize(book));
class Author
{
public required string Name { get; set; }
}
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
class Book
{
// Both of these properties are read-only.
public List<string> Contributors { get; } = new();
public Author Author { get; } = new() {Name = "Undefined"};
}
```
Before .NET 8, the output was like this:
```json
{"Contributors":[],"Author":{"Name":"Undefined"}}
```
With .NET 8, the output now looks like this:
```json
{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}}
```
## Disable reflection-based default
One of the nice features about Serialization is, now you can disable using the reflection-based serializer by default. To disable default reflection-based serialization, set the `JsonSerializerIsReflectionEnabledByDefault` MSBuild property to `false` in your project file.
## Streaming deserialization APIs
.NET 8 includes new `IAsyncEnumerable<T>` streaming deserialization extension methods. The new extension methods invoke streaming APIs and return `IAsyncEnumerable<T>`. Here is a sample code that uses this new feature.
```csharp
const string RequestUri = "https://yourwebsite.com/api/saas/tenants?skipCount=0&maxResultCount=10";
using var client = new HttpClient();
IAsyncEnumerable<Tenant> tenants = client.GetFromJsonAsAsyncEnumerable<Tenant>(RequestUri);
await foreach (Tenant tenant in tenants)
{
Console.WriteLine($"* '{tenant.name}' uses '{tenant.editionName
}' edition");
}
```
I have mentioned some of the items for Serialization Improvements in .NET 8. If you want to check the full list, you can read it in the .NET 8's [What's new document](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#serialization).

89
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md

@ -0,0 +1,89 @@
# Blazor's History and Full-stack Web UI
Blazor is a web framework that allows developers to build interactive web applications using .NET instead of JavaScript. The first version of Blazor was **released on May 14, 2020**. Since its initial release, Blazor has evolved with the new versions. Until now, six different versions have been declared. Sometimes, it can be not very clear to see the differences between these approaches. First, let's try to understand these.
* **Blazor-Server**: >> *Loads fast at first* >> In this version, heavy things are done in the server. Browsers are thin clients and download a small page for the first load. The page updates are done via SignalR connection. This was released with .NET Core 3.
* **Blazor WebAssembly (WASM):** >> *Loads slow at first* >> In this version, some binary files are being downloaded to the browser. This approach takes longer initialization time than the "Server" approach. The hard work is done on the browser.
* **Blazor Hybrid:** It's a combination of Blazor and Xamarin.Forms. It allows you to run your app on iOS, Android, macOS, and Windows. Blazor Hybrid uses a WebView component to host the Blazor-based user interface within the mobile app.
* **Blazor Native**: It runs natively on the devices and uses a common UI abstraction to render native controls for that device. This is very similar to how frameworks like Xamarin Forms or React Native work today. It has also been considered but has not reached [the planning stage](https://devblogs.microsoft.com/dotnet/blazor-server-in-net-core-3-0-scenarios-and-performance/).
* **Blazor United**: Recently, Microsoft updated this name to "Fullstack web UI". Blazor-Server and Blazor WASM both have some disadvantages and advantages. So, Microsoft decided to combine these two approaches and find an optimum solution for the entire Blazor version. We can call it *Best of Blazor* 😀
## Why is "Blazor Fullstack Web UI" the best?
These apps are a combination of both Blazor Server and Blazor WASM. It provides the advantages of Blazor Server and WASM. Developers would be able to more fine-tune the rendering mode. **This approach overcomes the large binary downloads of Blazor WASM, and it resolves the Blazor Server's problem, which always needs to be connected to the server via SignalR.**
> Blazor Fullstack Web UI comes with the .NET 8, and it will be published on November 14, 2023.
I took the following photo from Steven Sanderson's talk at NDC Porto 2023. You can read my impressions of this conf at https://volosoft.com/blog/ndc-port-2023-impressions, but after reading this one.
![image-20231106163046763](image-20231106163046763-1699282281622-2.png)
There are two basic page styles:
* **HTML**; simple and loads fast! Eg: MVC, Razor Pages
* **Single Page Apps (SPA):** high interaction with the client and loads slower! Eg: Blazor WASM, Blazor Server...
## HTML + SPA => Blazor Fullstack
Blazor Fullstack (formerly United) is a technology that turns Blazor Server into a SPA style.
.NET 8 will combine Blazor Server's server-side rendering and WebAssembly's client-side interaction.
You can switch between two rendering modes and even mix them on the same page. With .NET 8 there also comes amazing features like;
* [Streaming rendering](https://github.com/dotnet/aspnetcore/issues/46352): With this feature, most of the page will be rendered, and long async operations on the server will still be in progress.
* [Progressive enhancement of form submission & navigation](https://github.com/dotnet/aspnetcore/issues/46399): With this feature, it doesn't fully reload the page after submitting the form. This gives the user a better and smoother experience.
## How it works?
### Rendering on Server
You can add `WebComponentRenderMode.Server` to your Blazor components so that these components will run interactively. In the below example, the list editor will make AJAX requests to the server like single-page applications.
![image-20231106172420148](image-20231106172420148.png)
And sure you can add `WebComponentRenderMode.Server` to your page level, and the complete page will be rendered as a server component. All inputs on this page can work as an interactive server component like SPA mode.
![image-20231106172638604](image-20231106172638604.png)
### Rendering on client
You can switch to WebAssembly mode by writing `WebComponentRenderMode.WebAssembly` attribute to your page. By doing so, the whole page should run interactively using WebAssembly. This time there's no server connection anymore because it loads the binaries (WebAssembly runtimes) at the page load.
![image-20231106173021958](image-20231106173021958.png)
## How it works?
To enable Blazor Full-stack Web UI, you need to write `net8.0;net7.0-browser` into the `TargetFrameworks` area of your `csproj` file. These two keywords change your app like this; `net8.0` framework renders on the server, and `net7.0-browser` framework renders on the browser.
![image-20231106173411309](image-20231106173411309.png)
## Let the System decide on WebAssembly or Server approach
You can let the system decide whether it uses `WebAssembly` or `Server`. This can be done with the `Auto` mode of the `WebComponentRenderMode`. In this case, it will not load binary files (WebAssembly files) for the initial page that has `WebComponentRenderMode.Server` attribute, but whenever the user navigates to a page that has `WebComponentRenderMode.WebAssembly`, it will download the runtimes. This will allow us to load the initial page very fast, and when we need interactivity, we can switch to `WebAssembly` and wait for the binaries to download. But this download will be done one time because it will be cached.
![image-20231106173849303](image-20231106173849303.png)
## Conclusion
I summarized the new generation Blazor in a very simple way. This architecture will be useful to everyone who uses Blazor.
*Resources:*
* You can check out Dan Roth's GitHub issue 👉 [github.com/dotnet/aspnetcore/issues/46636](https://github.com/dotnet/aspnetcore/issues/46636).
* Steven Sanderson's YouTube video is very good for understanding these concepts 👉 [Blazor United Prototype Video](https://youtu.be/48G_CEGXZZM).

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

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.

2
docs/en/Timing.md

@ -102,7 +102,7 @@ This section covers the ABP Framework infrastructure related to managing time zo
### TimeZone Setting
ABP Framework defines **a setting**, named `Abp.Timing.Timezone`, that can be used to set and get the time zone for a user, [tenant](Multi-Tenancy.md) or globally for the application. The default value is `UTC`.
ABP Framework defines **a setting**, named `Abp.Timing.TimeZone`, that can be used to set and get the time zone for a user, [tenant](Multi-Tenancy.md) or globally for the application. The default value is `UTC`.
See the [setting documentation](Settings.md) to learn more about the setting system.

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:

27
docs/en/UI/Angular/Data-Table-Column-Extensions.md

@ -1,6 +1,5 @@
# Data Table Column (or Entity Prop) Extensions for Angular UI
## Introduction
Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below:
@ -196,6 +195,14 @@ type PropCallback<T, R = any> = (data?: PropData<T>) => R;
type PropPredicate<T> = (data?: PropData<T>) => boolean;
```
### ColumnPredicate
`ColumnPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `columnVisible` parameter. A column predicate gets a single parameter, the `GetInjected`, you can use the `GetInjected` parameter to reach injected `Service` or `Component`. The return type must be `boolean`. Here is a simplified representation:
```js
type ColumnPredicate<T> = (getInjected: GetInjected) => boolean;
```
### EntityPropOptions\<R = any\>
`EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop.
@ -212,6 +219,7 @@ type EntityPropOptions<R = any> = {
columnWidth?: number;
permission?: string;
visible?: PropPredicate<R>;
columnVisible?: ColumnPredicate;
};
```
@ -224,9 +232,12 @@ As you see, passing `type` and `name` is enough to create an entity prop. Here i
- **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`)
- **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`)
- **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`)
- **visible** is a predicate that will be used to decide if the cell content of this entity prop should be displayed on the table or not based on the data record. (_default:_ `() => true`)
- **columnVisible** is a predicate that will be used to decide if the column of this entity prop should be displayed on the table or not. (_default:_ `() => true`)
> Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell.
>
> `visible` predicate only hide the cell content, not the column. Use `columnVisible` to hide the entire column.
You may find a full example below.
@ -257,6 +268,10 @@ const options: EntityPropOptions<IdentityUserDto> = {
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
}
columnVisible: getInjected => {
const sessionStateService = getInjected(SessionStateService);
return !sessionStateService.getTenant()?.isAvailable; // hide this column when the tenant is available.
},
};
const prop = new EntityProp(options);
@ -281,19 +296,19 @@ The items in the list will be displayed according to the linked list order, i.e.
```js
export function reorderUserContributors(
propList: EntityPropList<IdentityUserDto>,
propList: EntityPropList<IdentityUserDto>
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, text) => prop.text === text,
(prop, text) => prop.text === text
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
(value, name) => value.name === name
);
}
```
@ -304,7 +319,7 @@ export function reorderUserContributors(
```js
export function isLockedOutPropContributor(
propList: EntityPropList<IdentityUserDto>,
propList: EntityPropList<IdentityUserDto>
) {
// add isLockedOutProp as 2nd column
propList.add(isLockedOutProp).byIndex(1);

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.

4
docs/zh-Hans/Timing.md

@ -102,7 +102,7 @@ var normalizedDateTime = Clock.Normalize(dateTime)
### 时区设置
ABP框架定义了一个名为 `Abp.Timing.Timezone` 的**设置**,可用于为应用程序的用户,[租户](Multi-Tenancy.md)或全局设置和获取时区. 默认值为 `UTC`.
ABP框架定义了一个名为 `Abp.Timing.TimeZone` 的**设置**,可用于为应用程序的用户,[租户](Multi-Tenancy.md)或全局设置和获取时区. 默认值为 `UTC`.
参阅[设置系统]了解更多关于设置系统.
@ -110,4 +110,4 @@ ABP框架定义了一个名为 `Abp.Timing.Timezone` 的**设置**,可用于为
`ITimezoneProvider` 是一个服务,可将[Windows时区ID](https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values)值简单转换为[Iana时区名称](https://www.iana.org/time-zones)值,反之亦然. 它还提供了获取这些时区列表与获取具有给定名称的 `TimeZoneInfo` 的方法.
它已使用[TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter)库实现.
它已使用[TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter)库实现.

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

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';

25
npm/ng-packs/packages/theme-shared/extensions/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/theme-shared/extensions/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 };

Loading…
Cancel
Save