Browse Source

Merge branch 'dev' into bundle-cdn

pull/17894/head
maliming 3 years ago
parent
commit
d585b2e7b2
  1. 4
      Directory.Packages.props
  2. 8
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  3. 16
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  4. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  5. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  6. 2
      docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md
  7. 30
      docs/en/Community-Articles/2023-10-23-NET-8-Feature-containers/POST.md
  8. 53
      docs/en/Community-Articles/2023-10-23-NET-8-Feature-raw-sql-queries-for-unmapped-types/POST.md
  9. 118
      docs/en/Community-Articles/2023-10-28-EF-Core-8-Primitive-Collections/POST.md
  10. 124
      docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md
  11. 196
      docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/POST.md
  12. BIN
      docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/built-in-metric-response.png
  13. BIN
      docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/built-in-metrics.png
  14. BIN
      docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/metrics-endpoint.png
  15. 156
      docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md
  16. 138
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md
  17. BIN
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png
  18. BIN
      docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png
  19. 189
      docs/en/Community-Articles/2023-11-05-EF-Core-8-Complex-Types/POST.MD
  20. 100
      docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md
  21. 89
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md
  22. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png
  23. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png
  24. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png
  25. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png
  26. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png
  27. BIN
      docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png
  28. 79
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md
  29. BIN
      docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png
  30. 75
      docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md
  31. 2
      docs/en/Deployment/Index.md
  32. 2
      docs/en/Migration-Guides/Abp-8_0.md
  33. 11
      docs/en/Modules/Cms-Kit/Blogging.md
  34. 1
      docs/en/Modules/Cms-Kit/Comments.md
  35. 3
      docs/en/Modules/Cms-Kit/Ratings.md
  36. 4
      docs/en/Startup-Templates/Application.md
  37. 2
      docs/en/Timing.md
  38. 8
      docs/en/Tutorials/Part-5.md
  39. 11
      docs/en/UI/Angular/Entity-Action-Extensions.md
  40. 1
      docs/en/UI/Angular/OAuth-Module.md
  41. 8
      docs/en/UI/Angular/Permission-Management.md
  42. 10
      docs/en/UI/Angular/Service-Proxies.md
  43. 4
      docs/zh-Hans/Startup-Templates/Application.md
  44. 4
      docs/zh-Hans/Timing.md
  45. 8
      docs/zh-Hans/Tutorials/Part-5.md
  46. 10
      docs/zh-Hans/UI/Angular/Permission-Management.md
  47. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardTagHelper.cs
  48. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardTagHelperService.cs
  49. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs
  50. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
  51. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelper.cs
  52. 9
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
  53. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelper.cs
  54. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
  55. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs
  56. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js
  57. 7
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.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. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs
  61. 3
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs
  62. 2
      framework/src/Volo.Abp.Imaging.ImageSharp/Volo.Abp.Imaging.ImageSharp.csproj
  63. 2
      framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpCompressOptions.cs
  64. 6
      framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpImageCompressorContributor.cs
  65. 6
      framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpImageResizerContributor.cs
  66. 10
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs
  67. 2
      framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/IAppUrlProvider.cs
  68. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  69. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  70. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  71. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  72. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  73. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  74. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  75. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  76. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  77. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  78. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  79. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  80. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  81. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  82. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  83. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  84. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  85. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  86. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json
  87. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json
  88. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json
  89. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json
  90. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json
  91. 4
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  92. 51
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs
  93. 11
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs
  94. 168
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs
  95. 2
      latest-versions.json
  96. 43
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  97. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml
  98. 2
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  99. 2
      modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs
  100. 2
      modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json

4
Directory.Packages.props

@ -138,8 +138,8 @@
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Shouldly" Version="4.0.3" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.6" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.2" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="2.0.0" />
<PackageVersion Include="Slugify.Core" Version="4.0.1" />
<PackageVersion Include="Spectre.Console" Version="0.47.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.6.122" />

8
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -223,6 +223,12 @@
"AbpStudio": "ABP Studio",
"ExtraMonths": "{0}<span>EXTRA MONTHS</span>",
"RSS": "RSS",
"RSSFeed": "RSS Feed"
"RSSFeed": "RSS Feed",
"Articles": "Articles",
"Organizations": "Organizations",
"ManageAccount": "Manage Account",
"CommunityProfile": "Community Profile",
"BlogProfile": "Blog Profile",
"Tickets": "Tickets"
}
}

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

6
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -248,6 +248,10 @@
"ShareOnTwitter": "Share on Twitter",
"ShareOnLinkedIn": "Share on LinkedIn",
"MoreFrom{0}": "More from {0}",
"SeeAllFrom{0}": "See all from {0}"
"SeeAllFrom{0}": "See all from {0}",
"MostWatched": "Most Watched",
"Articles({0})": "Articles ({0})",
"Videos({0})": "Videos ({0})",
"LatestArticles": "Latest Articles"
}
}

6
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -433,6 +433,10 @@
"PackageDetailPage_SeeDocumentation": "<a href=\"{0}\">See the documentation</a> to learn how to use this package in your applications.",
"PackageDetailPage_InstallingUsingPMC": "3: Installing with the Package Manager Console",
"PackageDetailPage_InstallingUsingPMCDescription1": "Open the <strong>Package Manager Console</strong> in Visual Studio (Tools -> Nuget Package Manager -> Package Manager Console) and execute the following command",
"UIOptions": "UI Options"
"UIOptions": "UI Options",
"Testimonials": "Testimonials",
"CoolestCompaniesUseABPFramework": "Coolest Companies Use <span class=\"fw-bold\">ABP Framework</span>",
"Index_Page_Testimonial_1": "ABP Framework is not just a tool but a catalyst that has accelerated my growth as a developer. It has made it possible for me to build new features faster than ever before, reminiscent of the experiences of other users. The unified coding pattern has streamlined my projects, giving me more time to focus on creating rather than troubleshooting.\nI would say the ABP Framework has been the cornerstone of my early professional journey. It has facilitated my transition from an aspiring developer to a confident professional ready to make a mark in the software world. I am looking forward to the exciting projects that await me, knowing that ABP will be there to guide me. It is more than just a product; it's a partner in success.",
"Index_Page_Testimonial_2": "ABP Framework is not only a framework, it is also a guidance for project development/management, because it provides DDD, GenericRepository, DI, Microservice, Modularity trainings. Even if you are not going to use framework itself, you can develop yourself with docs.abp.io which is well and professionally prepared. (OpenIddict, Redis, Quartz etc.)\nBecause many thing pre-built, it shortens project development time significantly. (Such as login page, exception handling, data filtering-seeding, audit logging, localization, auto api controller etc.)\nAs an example from our app, i have used Local Event Bus for stock control. So, I am able to manage order movements by writing stock handler.\nIt is wonderful not to lose time for CreationTime, CreatorId. They are filled automatically."
}
}

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 },
];

30
docs/en/Community-Articles/2023-10-23-NET-8-Feature-containers/POST.md

@ -0,0 +1,30 @@
# New Containers feature with NET 8.0
This article will show you the new feature of containers with NET 8.0.
## Non-root user
The `Non-root user` feature on net 8 is a security measure that allows users to have limited access to the system without having full administrative privileges. Hosting containers as `non-root` aligns with the principle of least privilege.
It’s free security provided by the operating system. If you run your app as root, your app process can do anything in the container, like modify files, install packages, or run arbitrary executables.
That’s a concern if your app is ever attacked. If you run your app as non-root, your app process cannot do much, greatly limiting what a bad actor could accomplish.
## Default ASP.NET Core port changed from 80 to 8080
In .NET 8, there has been a change in the default port used by ASP.NET Core applications. Previously, the default port assigned to ASP.NET Core applications was `80`. However, starting from .NET 8, the default port has been changed to `8080`.
This change was made to avoid conflicts with other applications and services that commonly use port 80, such as web servers like IIS or Apache. By using port 8080 as the default, there is less potential for clashes and easier deployment of ASP.NET Core applications alongside other services.
It's important to note that this change only affects the default port used when an ASP.NET Core application is run without explicitly specifying a port.
If you want your application to continue using port 80, you can still specify it during the application launch or configure it in the application settings.
* Recommended: Explicitly set the `ASPNETCORE_HTTP_PORTS`, `ASPNETCORE_HTTPS_PORTS``, and `ASPNETCORE_URLS` environment variables to the desired port. Example: `docker run --rm -it -p 9999:80 -e ASPNETCORE_HTTP_PORTS=80 <my-app>``
* Update existing commands and configuration that rely on the expected default port of port 80 to reference port 8080 instead. Example: `docker run --rm -it -p 9999:8080 <my-app>``
> The `dockerfile` of ABP templates has been updated to use port `80`.
## References
- [Secure your .NET cloud apps with rootless Linux Containers](https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/)
- [Containers breaking changes](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#containers)
- [ASP.NET Core apps use port 8080 by default](https://learn.microsoft.com/en-us/dotnet/core/compatibility/8.0#containers)
- [Docker images for ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/building-net-docker-images?view=aspnetcore-8.0)

53
docs/en/Community-Articles/2023-10-23-NET-8-Feature-raw-sql-queries-for-unmapped-types/POST.md

@ -0,0 +1,53 @@
# New Raw SQL queries for unmapped types feature with EF Core 8.0
## Introduction
I would love to talk about the new feature in EF Core 8.0, specifically the `raw SQL queries for unmapped types`.
This feature was recently introduced by Microsoft and is aimed at providing more flexibility and customization in database queries.
## What is the raw SQL queries for the unmapped types feature?
To give you a better understanding, let's look at a sample repository method with the ABP framework.
Here is an example of a raw SQL query using the new feature:
````csharp
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<List<AuthorIdWithNames>> GetAllAuthorNamesAsync();
}
public class AuthorIdWithNames
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class EfCoreAuthorRepository : EfCoreRepository<BookStoreDbContext, Author, Guid>, IAuthorRepository
{
public EfCoreAuthorRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public virtual async Task<List<AuthorIdWithNames>> GetAllAuthorNamesAsync()
{
return await (await GetDbContextAsync()).Database.SqlQuery<AuthorIdWithNames>(@$"SELECT Id, Name FROM Authors").ToListAsync();
}
}
````
In this code, we can see that we are using the `SqlQuery` method to execute a raw SQL query on a custom type, `AuthorIdWithNames` in this case. This allows us to retrieve data that may not be mapped to any of our entity classes in the context.
## In summary
This feature can be particularly useful in scenarios where we need to access data from tables or views that are not directly mapped to our entities. It also provides an alternative to using stored procedures for querying data.
However, it's important to note that using raw SQL queries can increase the risk of SQL injection attacks. So, it's recommended to use parameterized queries to prevent this. Additionally, this feature may not work with certain database providers, so it's important to check for compatibility before implementing it.
In conclusion, the raw SQL queries for unmapped types feature in EF Core 8.0 is a great addition for developers looking for more flexibility in database queries. It allows us to work with data that may not be directly mapped to our entities and can be a useful tool in certain scenarios. Just remember to use parameterized queries and check for compatibility before implementing it.
## References
- [Raw SQL queries for unmapped types](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#raw-sql-queries-for-unmapped-types)
- [SQL Queries](https://learn.microsoft.com/en-us/ef/core/querying/sql-queries#querying-scalar-(non-entity)-types)

118
docs/en/Community-Articles/2023-10-28-EF-Core-8-Primitive-Collections/POST.md

@ -0,0 +1,118 @@
# EF Core 8 Primitive collections
What can we do when we want to store a list of primitive types? Before EF Core 8, there were two options:
- Create a wrapper class and a related table, then add a foreign key linking each value to its owner of the collection.
- Use [value converter](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions) to serialize-deserialize as JSON.
The first option covers most scenarios if we need to add some additional properties to this type but let's say we're never gonna need this type additionality.
## Which collection types are supported ?
EF Core has the capability to map the `IEnumerable<T>` public properties that have both a getter and a setter, with the `T` representing a primitive type
```csharp
public class PrimitiveCollections
{
public IEnumerable<int> Ints { get; set; }
public ICollection<string> Strings { get; set; }
public ISet<DateTime> DateTimes { get; set; }
public IList<DateOnly> Dates { get; set; }
public uint[] UnsignedInts { get; set; }
public List<bool> Booleans { get; set; }
public List<Uri> Urls { get; set; }
}
```
> Some generic arguments are not considered primitive on the database side, such as `uint` and `Uri`. However, these types are also considered as primitive because there are [built-in value converters](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations#built-in-converters).
## Demo
In this sample, we have a `Car` class with a `Color` enum, and the `Car` class has a `Colors` property.
```csharp
public enum Color
{
Black,
White,
Red,
Blue
}
public class Car
{
public int Id { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public ISet<Color> Colors { get; set; } = new HashSet<Color>();
public Car(string brand, string model)
{
Brand = brand;
Model = model;
}
}
```
When we want to list the cars if they have any of the specific colors.
```csharp
var colors = new HashSet<Color> { Color.Blue, Color.White };
var cars = await context
.Cars
.Where(x=> x.Colors.Intersect(colors).Any())
.ToListAsync();
```
The SQL result looks like this; as you can see, it sends colors as parameters instead of adding them inline. It also uses the `json_each` function to deserialize on the database side.
```sql
SELECT "c"."Id", "c"."Brand", "c"."Colors", "c"."Model"
FROM "Cars" AS "c"
WHERE EXISTS (
SELECT 1
FROM (
SELECT "c0"."value"
FROM json_each("c"."Colors") AS "c0"
INTERSECT
SELECT "c1"."value"
FROM json_each(@__colors_0) AS "c1"
) AS "t")
```
When we insert to the car table.
```csharp
var car = new Car("Maserati", "GranTurismo")
{
Colors = new HashSet<Color>()
{
Color.Black,
Color.Blue
}
};
context.Cars.Add(car);
await context.SaveChangesAsync();
```
The SQL statement looks like this, and as you can see, it automatically serializes into the Colors parameter as JSON.
```sql
Executed DbCommand (0ms) [Parameters=
[@p0='Maserati' (Nullable = false) (Size = 8),
@p1='[0,3]' (Nullable = false) (Size = 5),
@p2='Flow' (Nullable = false) (Size = 4)
], CommandType='Text', CommandTimeout='30']
INSERT INTO "Cars" ("Brand", "Colors", "Model")
VALUES (@p0, @p1, @p2)
RETURNING "Id";
```
## Conclusion
We don't need to do anything if we just use a collection of primitive types. It serializes and deserializes them as JSON automatically. Additionally, it sends the primitive collection as a parameter to cache the query.
## References
- [EF Core 8 Primitive collections](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#primitive-collection-properties)
- [.NET Data Community Standup - Collections of primitive values in EF Core](https://www.youtube.com/watch?v=AUS2OZjsA2I)

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)

196
docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/POST.md

@ -0,0 +1,196 @@
# .NET 8 - ASP.NET Core Metrics
In this article, I'll show you the new built-in metrics of .NET 8, which are basically numerical measurements reported over time in your application and can be used to monitor the health of your application and generate reports according to those numerical values. We will see what the metrics are, why to use them, and how to use them in detail. So, let's dive in.
## What are Metrics?
Metrics are numerical measurements reported over time. These measurements are crucial for monitoring the health of an application and generating alerts when necessary. In the context of a web service, various metrics can be tracked, such as:
* Requests per second.
* Response time.
* Status code counts etc...
These metrics are not just collected; they are reported to a monitoring system at regular intervals. By doing so, the development and operations teams can visualize these metrics on dashboards. These dashboards provide a real-time overview of the application's performance and health.
## Pre-Built Metrics
ASP.NET Core has many built-in metrics. You can see the following figure for a list of built-in metrics for ASP.NET Core:
![](built-in-metrics.png)
> See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-aspnetcore and https://github.com/dotnet/aspnetcore/issues/47536 for all built-in metrics and their descriptions.
For example, we have the `kestrel-current-connections` metric that shows the _number of connections that are currently active on the server_ and the `http-server-request-duration` metric that shows _the duration of HTTP requests on the server_.
All of these and other built-in metrics are produced by using the [**System.Diagnostics.Metrics**](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics) API and with a small amount of code, we can use & view them.
## Using Pre-Built Metrics
Let's see the pre-built metrics in action.
Create a new ASP.NET Core app with the following command and change the directory to the created application folder:
```bash
dotnet new web -o MetricsDemo
cd MetricsDemo
```
After that, open the application in your favorite IDE and add the following service registration code to your application:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.WithMetrics(builder =>
{
builder.AddPrometheusExporter();
builder.AddMeter("Microsoft.AspNetCore.Hosting",
"Microsoft.AspNetCore.Server.Kestrel");
builder.AddView("http.server.request.duration",
new ExplicitBucketHistogramConfiguration
{
Boundaries = new double[] { 0, 0.005, 0.01, 0.025, 0.05,
0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 }
});
});
```
Here, we have done the following steps:
* The `AddOpenTelemetry` method registers the required **OpenTelemetry** services into the DI container.
* In the `WithMetrics` method, we add pre-built metrics to the `OpenTelemetryBuilder` in the `AddMeter` method (_Microsoft.AspNetCore.Hosting_ and _Microsoft.AspNetCore.Server.Kestrel_ are pre-built metrics provided by ASP.NET Core).
* We have also used the `AddView` method to customize the output of the metrics by the SDK. It can also be used to customize which [Instruments](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument) are to be processed or ignored.
After making the related configurations, let's add the middleware below and view the metrics:
```csharp
app.MapPrometheusScrapingEndpoint();
app.MapGet("/", () =>
{
Thread.Sleep(2000);
return "Hello, ABP Community member: " + DateTime.Now.Ticks.ToString()[^3..];
});
```
* `MapPrometheusScrapingEndpoint`: Adds the OpenTelemetry Prometheus scraping endpoint middleware to the pipeline and then we can see the all metrics by navigating to the **/metrics** endpoints (optional):
![](metrics-endpoint.png)
### View Metrics
You can use the `dotnet-counters` command-line tool, which allows you to view live metrics for .NET Core apps. You can run the following command to install the tool:
```bash
dotnet tool update -g dotnet-counters
```
After the tool is installed, you can run the application and by running the `dotnet-counters` tool in a terminal, you can monitor for the specific metrics:
```bash
dotnet-counters monitor -n MetricsDemo --counters Microsoft.AspNetCore.Hosting
```
When you run the command, it will print a message like the one below and will wait for an initial request to your application:
```txt
Press p to pause, r to resume, q to quit.
Status: Waiting for initial payload...
```
If you send a request to your application, then the related built-in metrics that you have added will be collected and will be shown in the terminal instantly:
![](built-in-metric-response.png)
Also, you can check for the other metric that we have used in the configuration, which is _Microsoft.AspNetCore.Server.Kestrel_ by setting it as a counter as in the command below, and when you send a request to your application, you will see different metrics:
```bash
dotnet-counters monitor -n MetricsDemo --counters Microsoft.AspNetCore.Server.Kestrel
```
## Creating Custom Metrics in ASP.NET Core Applications
So far, we have seen what metrics are, which built-in metrics are provided by ASP.NET Core, the use of the built-in metrics and running the `dotnet-counters` global tool to view these metrics. At this point, let's see how we can create custom metrics and view them via the `dotnet-counters` tool.
`IMeterFactory` is the recommended service to create *Meter* instances. ASP.NET Core registers the `IMeterFactory` in dependency injection (DI) by default. The meter factory integrates metrics with DI, making isolating and collecting metrics easy.
Assume that, we want to create a metric that holds the sold products in our application. For that purpose, as a first step, we can create a metric class as follows:
```csharp
using System.Diagnostics.Metrics;
namespace MetricsDemo;
public class ProductMetrics
{
private readonly Counter<int> _soldProductsCount;
public ProductMetrics(IMeterFactory meterFactory)
{
var meterInstance = meterFactory.Create("MetricsDemo.ProductStore");
_soldProductsCount = meterInstance.CreateCounter<int>("metricsdemo.productstore.sold_products_count");
}
public void IncreaseSoldProductCount(int quantity)
{
_soldProductsCount.Add(quantity);
}
}
```
In this class, we are creating a counter and defining metrics that we want to collect and view. `MetricsDemo.ProductStore` is the **counter name** and `metricsdemo.productstore.sold_products_count` is the **metric**. In our example, we need to register this class with a `Singleton` lifetime because we don't want to reset the counter and want to keep it during the application runtime.
> The [System.Diagnostics.Metrics.Meter](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.meter) type is the entry point for a library to create a named group of instruments. Instruments record the numeric measurements that are needed to calculate metrics. Here we used [CreateCounter](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.meter.createcounter) to create a Counter instrument named _metricsdemo.productstore.sold_products_count_.
So, register the type with the DI container in the `Program.cs` file (or you can implement the `ISingletonDependency` interface, if you are using an ABP based application):
```csharp
builder.Services.AddSingleton<ProductMetrics>();
```
Then, we can inject this class and increase the sold product count whenever it's needed. Since we have registered it in DI, we can use it in minimal APIs as in the following example:
```csharp
app.MapPost("/complete-sale", ([FromBody] ProductSoldModel model, ProductMetrics metrics) =>
{
// ... business logic such as saving the product to the database etc... ...
metrics.IncreaseSoldProductCount(model.Quantity);
});
```
Whenever a request is made to this endpoint the metric that we've defined (_metricsdemo.productstore.sold_products_count_) will be increased and we can see this metric via the `dotnet-counters` global tool. You can run the following command to see the metrics:
```bash
dotnet-counters monitor -n MetricsDemo --counters MetricsDemo.ProductStore
```
Here is an example output that you would see if you have made a POST request to the `/complete-sale` endpoint:
```txt
Press p to pause, r to resume, q to quit.
Status: Paused
[MetricsDemo.ProductStore]
metricsdemo.productstore.sold_products_count (Count / 1 sec) 20
```
### Advanced: View Metrics in Prometheus & Grafana - Building Dashboards
It's a common requirement to build a dashboard and monitor the metrics and check the health of your applications. At that point, you can expose the metrics and scrape them using the [Prometheus](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0#set-up-and-configure-prometheus) and build a dashboard by using the [Grafana](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0#show-metrics-on-a-grafana-dashboard).
If you want to build a dashboard, you can see [this documentation from Microsoft](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0#view-metrics-in-grafana-with-opentelemetry-and-prometheus), which shows how to set up the **Prometheus server** and show metrics on a **Grafana** dashboard.
Also, you can check [this video](https://www.youtube.com/watch?v=A2pKhNQoQUU) if you want to see it in action.
## Conclusion
In this article, I've shown you the new built-in metrics of .NET8, described what metrics are, which pre-built metrics are provided by ASP.NET Core, how to use & view these pre-built metrics and finally, I have shown you how to create custom metrics and view them in a dashboard. If you want to learn more about the **Metrics System of .NET**, you can check out the references that I have shared below.
## References
* https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-8.0
* https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-5/
* https://www.youtube.com/watch?v=A2pKhNQoQUU

BIN
docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/built-in-metric-response.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/built-in-metrics.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Community-Articles/2023-10-30-NET-8-ASP-NET-Core-Metrics/metrics-endpoint.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

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

189
docs/en/Community-Articles/2023-11-05-EF-Core-8-Complex-Types/POST.MD

@ -0,0 +1,189 @@
# Using Complex Types as Value Objects with Entity Framework Core 8.0
Entity Framework Core 8.0 is being shipped in a week as a part of .NET 8.0. In this article, I will introduce the new **[Complex Types](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#value-objects-using-complex-types)** feature of EF Core 8 and show some examples of how you can use it in your projects built with ABP Framework.
## What is a Value Object?
A [Value Object](https://docs.abp.io/en/abp/latest/Value-Objects) is a simple object that has no conceptual identity (Id). Instead, a Value Object is identified by its properties.
A Value Object is typically owned by a parent [Entity](https://docs.abp.io/en/abp/latest/Entities) object. Using [Complex Types](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#value-objects-using-complex-types) with EF Core 8 is the best way to create Value Objects that are stored as a part of your main entity.
## Creating an ABP Project
> **WARNING**: ABP Framework has not a .NET 8.0 compatible version yet. It is planned to be released on November 15, 2023. I created this project with ABP 7.4.1 (based on .NET 7.0), then manually changed the `TargetFramework` (in the `csproj`) to `net8.0` and added the `Microsoft.EntityFrameworkCore.SqlServer` package with version `8.0.0-rc.2.23480.1`. After that, the project is being compiled, but some parts of this article may not work as expected until ABP Framework 8.0-RC.1 is released.
>
> **I will update the article once ABP Framework 8.0-RC.1 is released.**
I will show code examples, so I am creating a new ABP project using the following [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) command:
````bash
abp new ComplexTypeDemo -t app-nolayers
````
> I prefer a non-layered project to keep the things simple. If you are new to ABP Framework, follow the [Getting Started](https://docs.abp.io/en/abp/latest/Getting-Started-Overall) tutorial to learn how to create a new project from scratch.
Once I created the solution, I am running the following command to execute the database migrations in order to create the initial database:
````bash
dotnet run --project ComplexTypeDemo --migrate-database
````
> We could also execute the `migrate-database.ps1` (that is coming as a part of the solution) as a shortcut.
## Creating a Complex Type
Assume that we have a `Customer` [entity](https://docs.abp.io/en/abp/latest/Entities) as shown below:
````csharp
public class Customer : BasicAggregateRoot<Guid>
{
public string Name { get; set; }
public Address HomeAddress { get; set; }
public Address BusinessAddress { get; set; }
}
````
Here, we have two address properties, one for home and the other one for business. Since an address is a multi-values property, we can define it as a separate object:
````csharp
public class Address
{
public string City { get; set; }
public string Line1 { get; set; }
public string? Line2 { get; set; }
public string PostCode { get; set; }
}
````
`Address` is a typical complex object. It is actually a part of the `Customer` object, but we wanted to collect its parts into a dedicated class to make it a domain concept and easily manage its properties together.
## Configure EF Core Mappings
We should set the `Address` class as a Complex Type in our EF Core mapping configuration. There are two ways of it.
As the first way, we can add the `ComplexType` attribute on top of the `Address` class:
````csharp
[ComplexType] // Added this line
public class Address
{
...
}
````
As an alternative, we can configure the mapping using the fluent mapping API. You can write the following code into the `OnModelCreating` method of your `DbContext` class:
````csharp
builder.Entity<Customer>(b =>
{
b.ToTable("Customers");
b.ComplexProperty(x => x.HomeAddress); // Mapping a Complex Type
b.ComplexProperty(x => x.BusinessAddress); // Mapping another Complex Type
//... configure other properties
});
````
You can further configure the properties of the `Address` class:
````csharp
b.ComplexProperty(x => x.HomeAddress, a =>
{
a.Property(x => x.City).HasMaxLength(50).IsRequired();
});
````
Once you configure the mappings, you can use the [EF Core command-line tool](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) to create a database migration:
````bash
dotnet ef migrations add "Added_Customer_And_Address"
````
And update the database:
````bash
dotnet ef database update
````
If you check the fields of the `Customers` table in your dayabase, you will see the following fields:
* `Id`
* `Name`
* `HomeAddress_City`
* `HomeAddress_Line1`
* `HomeAddress_Line2`
* `HomeAddress_PostCode`
* `BusinessAddress_City`
* `BusinessAddress_Line1`
* `BusinessAddress_Line2`
* `BusinessAddress_PostCode`
As you see, EF Core stores the `Address` properties as a part of your main entity.
## Querying Objects
You can query entities from database using the properties of a complex type as same as you query by the properties of the main entity.
**Example: Find customers by `BusinessAddress.PostCode`:**
````csharp
public class MyService : ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public MyService(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task DemoAsync()
{
var customers = await _customerRepository.GetListAsync(
c => c.BusinessAddress.PostCode == "12345"
);
//...
}
}
````
## Mutable vs Immutable
Entity Framework Core Complex Types can work with mutable and immutable types. In the `Address` example above, I've shown a simple mutable class - that means you can change an individual property of an `Address` object after creating it (or after querying from database). However, designing Value Objects as immutable is a highly common approach.
For example, you can use C#'s `struct` type to define an immutable `Address` type:
````csharp
public readonly struct Address(string line1, string? line2, string city, string postCode)
{
public string City { get; } = city;
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string PostCode { get; } = postCode;
}
````
See the [Microsoft's documentation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#mutability) for more examples and different usages.
## Final Notes
There are more details about using Complex Types in your applications. I want to mention some of them here:
* You can have nested complex types. For example, `Address` may have one or more `PhoneNumber` objects as its properties. Everything will work seamlessly.
* A single instance of a Complex Type can be set to multiple properties (of the same or different entities). In that case, changing the Complex object's properties will affect all of the properties. However, try to avoid that since it may create unnecessary complexities in your code that is hard to understand.
* You can manipulate mutable complex object properties just as another property in your entity. EF Core change tracking system will track them as you expect.
* Currently, EF Core doesn't support to have a collection of complex objects in an entity. It works only for properties.
For more details and examples, see the Microsoft's document in the *References* section.
## Source Code
You can find the sample project here:
https://github.com/hikalkan/samples/tree/master/EfCoreComplexTypeDemo
## References
* [Value objects using Complex Types](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#value-objects-using-complex-types) in [What's new with EF Core 8.0](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew) document by Microsoft.
* [ABP Entity Framework Core integration document](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)

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.

2
docs/en/Deployment/Index.md

@ -7,7 +7,7 @@ However, there are some topics that you should care about when you are deploying
## Guides
* [Configuring SSL certificate(HTTPS)](SSL.md): Explains how to configure SSL certificate(HTTPS) for your application.
* [Configuring for OpenIddict](Configuring-OpenIddict.md): Notes for some essential configurations for OpenIddict.
* [Configuring OpenIddict](Configuring-OpenIddict.md): Notes for some essential configurations for OpenIddict.
* [Configuring for Production](Configuring-Production.md): Notes for some essential configurations for production environments.
* [Optimization for Production](Optimizing-Production.md): Tips and suggestions for optimizing your application on production environments.
* [Deploying to a Clustered Environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently.

2
docs/en/Migration-Guides/Abp-8_0.md

@ -66,7 +66,7 @@ The following NuGet libraries have been upgraded:
| Serilog.Sinks.Async | 1.4.0 | 1.5.0 |
| SharpZipLib | 1.3.3 | 1.4.2 |
| Shouldly | 4.1.0 | 4.2.1 |
| SixLabors.ImageSharp | 1.0.4 | 2.1.6 |
| SixLabors.ImageSharp | 1.0.4 | 3.0.2 |
| Slugify.Core | 3.0.0 | 4.0.1 |
| Spectre.Console | 0.46.1-preview.0.7 | 0.47.0 |
| Swashbuckle.AspNetCore | 6.2.1 | 6.5.0 |

11
docs/en/Modules/Cms-Kit/Blogging.md

@ -29,7 +29,16 @@ A screenshot from the new blog creation modal:
![blogs-edit](../../images/cmskit-module-blogs-edit.png)
**Slug** is the URL part of the blog. For this example, the root URL of the blog becomes *https://your-domain.com/blogs/technical-blog/*.
**Slug** is the URL part of the blog. For this example, the root URL of the blog becomes `your-domain.com/blogs/technical-blog/`.
- You can change the default slug by using `CmsBlogsWebConsts.BlogRoutePrefix` constant. For example, if you set it to `foo`, the root URL of the blog becomes `your-domain.com/foo/technical-blog/`.
```csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
CmsBlogsWebConsts.BlogsRoutePrefix = "foo";
}
```
#### Blog Features

1
docs/en/Modules/Cms-Kit/Comments.md

@ -53,6 +53,7 @@ The comment system provides a commenting [widget](../../UI/AspNetCore/Widgets.md
{
entityType = "Product",
entityId = "...",
isReadOnly = false,
referralLinks = new [] {"nofollow"}
})
```

3
docs/en/Modules/Cms-Kit/Ratings.md

@ -41,7 +41,8 @@ The ratings system provides a rating widget to allow users send ratings to resou
@await Component.InvokeAsync(typeof(RatingViewComponent), new
{
entityType = "Product",
entityId = "entityId"
entityId = "entityId",
isReadOnly = false
})
```

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:

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
},

10
docs/en/UI/Angular/Service-Proxies.md

@ -73,6 +73,16 @@ export const environment: Config.Environment = {
> The destination the `proxy` folder is created and the paths above may change based on your project structure.
## Parameters of generate-proxy
- **module or -m:** The backend module name. The default is `app`. The object key of the modules defined in response of `api/abp/api-definition`. For example, if you want to generate-proxy of PermissionManagement, you should pass `permissionManagement` as a value.
- **apiName or -a:** The Backend api name, also known as remoteServiceName. It is defined in the selected module (in response of `api/abp/api-definition`). The property(key) name is `remoteServiceName`. For example for the PermissionManagement, you should pass `AbpPermissionManagement`
- **source:** Source of the Angular project for the API definition URL & root namespace resolution.
- **target:** Target for the Angular project to place the generated code. For example, if it's `permission-management`, it'll look like this (npm/ng-packs/packages/*permission-management*).
- **entryPoint:** To create the generated proxy folder in the target. The directory is `permission-management/proxy/src/lib/proxy` and the `permission-management` is the value of target. If you want to create a folder for the generated proxy, there are two options, you should either set the value `proxy` as the entryPoint or go to project.json and change the `sourceRoot` from `packages/permission-management/src` to `packages/permission-management/proxy/src`. No need to change the sourceRoot of project with the property. if you keep it empty, the proxy will be generated into the folder defined in the sourceRoot property.
- **serviceType:** The service type of the generated proxy. The options are `application`, `integration` and `all`. The default value is `application`. A developer can mark a service "integration service". If you want to skip proxy generation for the service, then this is the correct setting. More info about [Integration Services](../../Integration-Services)
### Services
The `generate-proxy` command generates one service per back-end controller and a method (property with a function value actually) for each action in the controller. These methods call backend APIs via [RestService](./Http-Requests#restservice).

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
},

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardTagHelper.cs

@ -4,6 +4,8 @@ public class AbpCardTagHelper : AbpTagHelper<AbpCardTagHelper, AbpCardTagHelperS
{
public AbpCardBorderColorType Border { get; set; } = AbpCardBorderColorType.Default;
public bool AddMarginBottomClass { get; set; } = true;
public AbpCardTagHelper(AbpCardTagHelperService tagHelperService)
: base(tagHelperService)
{

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardTagHelperService.cs

@ -8,7 +8,11 @@ public class AbpCardTagHelperService : AbpTagHelperService<AbpCardTagHelper>
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.Attributes.AddClass("card mb-3");
output.Attributes.AddClass("card");
if (TagHelper.AddMarginBottomClass)
{
output.Attributes.AddClass("mb-3");
}
SetBorder(context, output);
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs

@ -52,6 +52,8 @@ public class AbpInputTagHelper : AbpTagHelper<AbpInputTagHelper, AbpInputTagHelp
public CheckBoxHiddenInputRenderMode? CheckBoxHiddenInputRenderMode { get; set; }
public bool AddMarginBottomClass { get; set; } = true;
public AbpInputTagHelper(AbpInputTagHelperService tagHelperService)
: base(tagHelperService)
{

8
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs

@ -58,7 +58,10 @@ public class AbpInputTagHelperService : AbpTagHelperService<AbpInputTagHelper>
{
output.Attributes.AddClass("form-floating");
}
output.Attributes.AddClass(isCheckBox ? "mb-2" : "mb-3");
if (TagHelper.AddMarginBottomClass)
{
output.Attributes.AddClass(isCheckBox ? "mb-2" : "mb-3");
}
if (isCheckBox)
{
output.Attributes.AddClass("custom-checkbox");
@ -110,7 +113,8 @@ public class AbpInputTagHelperService : AbpTagHelperService<AbpInputTagHelper>
protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml, bool isCheckbox)
{
return "<div class=\"" + (isCheckbox ? "custom-checkbox custom-control mb-2 form-check" : "mb-3") + "\">" +
var mb = TagHelper.AddMarginBottomClass ? (isCheckbox ? "mb-2" : "mb-3") : string.Empty;
return "<div class=\"" + (isCheckbox ? $"custom-checkbox custom-control {mb} form-check" : $"{mb}") + "\">" +
Environment.NewLine + innerHtml + Environment.NewLine +
"</div>";
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelper.cs

@ -37,7 +37,7 @@ public class AbpSelectTagHelper : AbpTagHelper<AbpSelectTagHelper, AbpSelectTagH
public string? AutocompleteSelectedItemName { get; set; }
public string? AutocompleteSelectedItemValue { get; set; }
public string? AutocompleteParentSelector { get; set; }
public string? AllowClear { get; set; }
@ -48,6 +48,8 @@ public class AbpSelectTagHelper : AbpTagHelper<AbpSelectTagHelper, AbpSelectTagH
public bool FloatingLabel { get; set; }
public bool AddMarginBottomClass { get; set; } = true;
public AbpSelectTagHelper(AbpSelectTagHelperService tagHelperService)
: base(tagHelperService)
{

9
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs

@ -59,7 +59,11 @@ public class AbpSelectTagHelperService : AbpTagHelperService<AbpSelectTagHelper>
{
output.Attributes.AddClass("form-floating");
}
output.Attributes.AddClass("mb-3");
if (TagHelper.AddMarginBottomClass)
{
output.Attributes.AddClass("mb-3");
}
output.TagMode = TagMode.StartTagAndEndTag;
output.Content.SetHtmlContent(innerHtml);
}
@ -79,7 +83,8 @@ public class AbpSelectTagHelperService : AbpTagHelperService<AbpSelectTagHelper>
protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml)
{
return "<div class=\"mb-3\">" + Environment.NewLine + innerHtml + Environment.NewLine + "</div>";
var mb3 = TagHelper.AddMarginBottomClass ? "mb-3" : string.Empty;
return $"<div class=\"{mb3}\">" + Environment.NewLine + innerHtml + Environment.NewLine + "</div>";
}
protected virtual async Task<TagHelperOutput> GetSelectTagAsync(TagHelperContext context, TagHelperOutput output, TagHelperContent childContent)

14
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelper.cs

@ -21,20 +21,20 @@ public abstract class
public bool LabelTooltipHtml { get; set; } = false;
[HtmlAttributeName("info")]
[HtmlAttributeName("info")]
public string? InfoText { get; set; }
[HtmlAttributeName("disabled")]
[HtmlAttributeName("disabled")]
public bool IsDisabled { get; set; } = false;
[HtmlAttributeName("readonly")]
[HtmlAttributeName("readonly")]
public bool? IsReadonly { get; set; } = false;
public bool AutoFocus { get; set; }
public AbpFormControlSize Size { get; set; } = AbpFormControlSize.Default;
[HtmlAttributeName("required-symbol")]
[HtmlAttributeName("required-symbol")]
public bool DisplayRequiredSymbol { get; set; } = true;
public string? Name { get; set; }
@ -43,11 +43,13 @@ public abstract class
public bool SuppressLabel { get; set; }
public bool AddMarginBottomClass { get; set; } = true;
protected AbpDatePickerBaseTagHelper(AbpDatePickerBaseTagHelperService<TTagHelper> service) : base(service)
{
_abpDatePickerOptionsImplementation = new AbpDatePickerOptions();
}
public void SetDatePickerOptions(IAbpDatePickerOptions options)
{
_abpDatePickerOptionsImplementation = options;
@ -217,4 +219,4 @@ public abstract class
get => _abpDatePickerOptionsImplementation.Options;
set => _abpDatePickerOptionsImplementation.Options = value;
}
}
}

14
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs

@ -22,7 +22,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker;
public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelperService<TTagHelper>
where TTagHelper : AbpDatePickerBaseTagHelper<TTagHelper>
{
protected readonly Dictionary<Type, Func<object, string>> SupportedInputTypes = new()
protected readonly Dictionary<Type, Func<object, string>> SupportedInputTypes = new()
{
{
typeof(string), o =>
@ -42,7 +42,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
{
return dt.ToString("O");
}
return string.Empty;
}
},
@ -54,7 +54,7 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
{
return dto.ToString("O");
}
return string.Empty;
}
},
@ -161,7 +161,10 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
output.TagMode = TagMode.StartTagAndEndTag;
output.TagName = "div";
LeaveOnlyGroupAttributes(context, output);
output.Attributes.AddClass("mb-3");
if (TagHelper.AddMarginBottomClass)
{
output.Attributes.AddClass("mb-3");
}
output.Content.AppendHtml(innerHtml);
}
@ -224,7 +227,8 @@ public abstract class AbpDatePickerBaseTagHelperService<TTagHelper> : AbpTagHelp
protected virtual string SurroundInnerHtmlAndGet(TagHelperContext context, TagHelperOutput output, string innerHtml)
{
return "<div class=\"mb-3\">" +
var mb = TagHelper.AddMarginBottomClass ? "mb-3" : string.Empty;
return $"<div class=\"{mb}\">" +
Environment.NewLine + innerHtml + Environment.NewLine +
"</div>";
}

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,

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js

@ -135,7 +135,7 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
_args = args || {};
var argsWithoutFunc = {};
for (a in _args) {
for (var a in _args) {
if (_args.hasOwnProperty(a) && typeof _args[a] !== 'function') {
argsWithoutFunc[a] = _args[a];
}
@ -144,7 +144,8 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f
_createContainer(_modalId)
.load(options.viewUrl, $.param(argsWithoutFunc), function (response, status, xhr) {
if (status === "error") {
//TODO: Handle!
var responseJSON = xhr.responseJSON ? xhr.responseJSON : JSON.parse(xhr.responseText);
abp.ajax.showError(responseJSON.error ? responseJSON.error : abp.ajax.defaultError);
return;
};

7
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs

@ -538,8 +538,11 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase<CSharpServi
usingNamespaceList?.AddIfNotContains($"using {GetTypeNamespace(typeName)};");
return NormalizeTypeName(typeName.Split(".").Last());
}
AddGenericTypeUsingNamespace(typeName, usingNamespaceList);
if(usingNamespaceList != null)
{
AddGenericTypeUsingNamespace(typeName, usingNamespaceList);
}
var type = new StringBuilder();
var s1 = typeName.Split("<");

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

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs

@ -19,7 +19,7 @@ public class AbpJsonValueConverter<TPropertyType> : ValueConverter<TPropertyType
return JsonSerializer.Serialize(d);
}
private static readonly JsonSerializerOptions DeserializeOptions = new JsonSerializerOptions()
public readonly static JsonSerializerOptions DeserializeOptions = new JsonSerializerOptions()
{
Converters =
{

3
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Volo.Abp.Data;
using Volo.Abp.Json.SystemTextJson.JsonConverters;
@ -41,7 +40,7 @@ public class ExtraPropertiesValueConverter : ValueConverter<ExtraPropertyDiction
return JsonSerializer.Serialize(copyDictionary);
}
private static readonly JsonSerializerOptions DeserializeOptions = new JsonSerializerOptions()
public static readonly JsonSerializerOptions DeserializeOptions = new JsonSerializerOptions()
{
Converters =
{

2
framework/src/Volo.Abp.Imaging.ImageSharp/Volo.Abp.Imaging.ImageSharp.csproj

@ -4,7 +4,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<PackageId>Volo.Abp.Imaging.ImageSharp</PackageId>

2
framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpCompressOptions.cs

@ -22,7 +22,7 @@ public class ImageSharpCompressOptions
PngEncoder = new PngEncoder {
CompressionLevel = PngCompressionLevel.BestCompression,
IgnoreMetadata = true
SkipMetadata = true
};
WebpEncoder = new WebpEncoder {

6
framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpImageCompressorContributor.cs

@ -34,14 +34,14 @@ public class ImageSharpImageCompressorContributor : IImageCompressorContributor,
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported);
}
var (image, format) = await Image.LoadWithFormatAsync(stream, cancellationToken);
var image = await Image.LoadAsync(stream, cancellationToken);
if (!CanCompress(format.DefaultMimeType))
if (!CanCompress(image.Metadata.DecodedImageFormat!.DefaultMimeType))
{
return new ImageCompressResult<Stream>(stream, ImageProcessState.Unsupported);
}
var memoryStream = await GetStreamFromImageAsync(image, format, cancellationToken);
var memoryStream = await GetStreamFromImageAsync(image, image.Metadata.DecodedImageFormat, cancellationToken);
if (memoryStream.Length < stream.Length)
{

6
framework/src/Volo.Abp.Imaging.ImageSharp/Volo/Abp/Imaging/ImageSharpImageResizerContributor.cs

@ -24,9 +24,9 @@ public class ImageSharpImageResizerContributor : IImageResizerContributor, ITran
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported);
}
var (image, format) = await Image.LoadWithFormatAsync(stream, cancellationToken);
var image = await Image.LoadAsync(stream, cancellationToken);
if (!CanResize(format.DefaultMimeType))
if (!CanResize(image.Metadata.DecodedImageFormat!.DefaultMimeType))
{
return new ImageResizeResult<Stream>(stream, ImageProcessState.Unsupported);
}
@ -44,7 +44,7 @@ public class ImageSharpImageResizerContributor : IImageResizerContributor, ITran
try
{
await image.SaveAsync(memoryStream, format, cancellationToken: cancellationToken);
await image.SaveAsync(memoryStream, image.Metadata.DecodedImageFormat, cancellationToken: cancellationToken);
memoryStream.Position = 0;
return new ImageResizeResult<Stream>(memoryStream, ImageProcessState.Done);
}

10
framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/AppUrlProvider.cs

@ -53,6 +53,16 @@ public class AppUrlProvider : IAppUrlProvider, ITransientDependency
return allow;
}
public virtual async Task<string?> NormalizeUrlAsync(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return url;
}
return await ReplacePlaceHoldersAsync(url!);
}
protected virtual async Task<string> GetConfiguredUrl(string appName, string? urlName)
{
var url = await GetUrlOrNullAsync(appName, urlName);

2
framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Urls/IAppUrlProvider.cs

@ -10,4 +10,6 @@ public interface IAppUrlProvider
Task<string?> GetUrlOrNullAsync([NotNull] string appName, string? urlName = null);
bool IsRedirectAllowedUrl(string url);
Task<string?> NormalizeUrlAsync(string? url);
}

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

2
modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingDbContextModelBuilderExtensions.cs

@ -73,7 +73,7 @@ public static class AbpAuditLoggingDbContextModelBuilderExtensions
b.ConfigureByConvention();
b.Property(x => x.EntityTypeFullName).HasMaxLength(EntityChangeConsts.MaxEntityTypeFullNameLength).IsRequired().HasColumnName(nameof(EntityChange.EntityTypeFullName));
b.Property(x => x.EntityId).HasMaxLength(EntityChangeConsts.MaxEntityIdLength).IsRequired().HasColumnName(nameof(EntityChange.EntityId));
b.Property(x => x.EntityId).HasMaxLength(EntityChangeConsts.MaxEntityIdLength).HasColumnName(nameof(EntityChange.EntityId));
b.Property(x => x.AuditLogId).IsRequired().HasColumnName(nameof(EntityChange.AuditLogId));
b.Property(x => x.ChangeTime).IsRequired().HasColumnName(nameof(EntityChange.ChangeTime));
b.Property(x => x.ChangeType).IsRequired().HasColumnName(nameof(EntityChange.ChangeType));

2
modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json

@ -3,7 +3,7 @@
"name": "asp.net",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.shared": "~7.4.0",
"@abp/aspnetcore.mvc.ui.theme.shared": "~7.4.1",
"@abp/prismjs": "^~7.3.2",
"@abp/highlight.js": "^~7.3.2"
},

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

Loading…
Cancel
Save