@ -0,0 +1,14 @@ |
|||
### Description |
|||
|
|||
Resolves #xxxx (write the related issue number if available) |
|||
|
|||
TODO: Describe what this PR has changed, add screenshot or animated GIF if available, write if it is a breaking change, and how to fix the breaking changes for existing applications if so. |
|||
|
|||
### Checklist |
|||
|
|||
- [ ] I fully tested it as developer / designer and created unit / integration tests |
|||
- [ ] I documented it (or no need to document or I will create a separate documentation issue) |
|||
|
|||
### How to test it? |
|||
|
|||
Please describe how this can be tested by the test engineers if it is not already explicit - or remove this section if no need to description. |
|||
@ -1,31 +0,0 @@ |
|||
name: Documentation Checks |
|||
|
|||
on: |
|||
push: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
# This ensures the check will only be run when something changes in the docs content |
|||
- "docs/en/**/*" |
|||
pull_request: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
- "docs/en/**/*" |
|||
permissions: |
|||
contents: read # to fetch code (actions/checkout) |
|||
jobs: |
|||
spellcheck: |
|||
name: "Docs: Spellcheck (En)" |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v2 |
|||
name: Check out the code |
|||
- uses: actions/setup-node@v1 |
|||
name: Setup node |
|||
with: |
|||
node-version: "16" |
|||
- run: npm install -g cspell |
|||
name: Install cSpell |
|||
- run: cspell --config ./cSpell.json "docs/en/**/*.md" --no-progress # Update for path to the markdown files |
|||
name: Run cSpell |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "ar", |
|||
"texts": { |
|||
"AbpTitle": "إطار عمل ABP - إطار عمل تطبيق ويب مفتوح المصدر", |
|||
"AbpDescription": "ABP هو إطار عمل مفتوح المصدر يركز على تطوير تطبيقات الويب القائمة على AspNet Core. لا تكرر نفسك ، ركز على كود عملك الخاص." |
|||
} |
|||
} |
|||
@ -1,151 +0,0 @@ |
|||
{ |
|||
"version": "0.2", |
|||
"language": "en", |
|||
"words": [ |
|||
"ABP's", |
|||
"abpframework", |
|||
"Antiforgery", |
|||
"appsettings", |
|||
"aspnet", |
|||
"aspnetcore", |
|||
"Autofac", |
|||
"automagically", |
|||
"Blazor", |
|||
"CQRS", |
|||
"crossfade", |
|||
"Dapr", |
|||
"Datagrid's", |
|||
"Datatable", |
|||
"datepicker", |
|||
"dismissable", |
|||
"dockerized", |
|||
"entrypoints", |
|||
"findability", |
|||
"hoverable", |
|||
"Iddict", |
|||
"IntelliCode", |
|||
"Keysize", |
|||
"Linq", |
|||
"Microservices", |
|||
"middlewares", |
|||
"Minifier", |
|||
"multitenancy", |
|||
"multitenant", |
|||
"Navs", |
|||
"Newtonsoft", |
|||
"Npgsql", |
|||
"oidc", |
|||
"overridable", |
|||
"Parameterless", |
|||
"Passwordless", |
|||
"PKCE", |
|||
"preconfigured", |
|||
"proxying", |
|||
"redirections", |
|||
"scrollbars", |
|||
"signin", |
|||
"Templating", |
|||
"textboxes", |
|||
"toolset", |
|||
"unsubscription", |
|||
"Xunit" |
|||
], |
|||
"ignoreWords": [ |
|||
"Aliyun", |
|||
"Allibone", |
|||
"Blazorise", |
|||
"Boutwell", |
|||
"Cmskit", |
|||
"connectionstrings", |
|||
"Devart", |
|||
"Formik", |
|||
"Halil", |
|||
"Hanselman", |
|||
"hikalkan", |
|||
"Ibrahim", |
|||
"İbrahim", |
|||
"Kalkan", |
|||
"Kirti", |
|||
"Kommunity", |
|||
"Kulkarni", |
|||
"Luxon", |
|||
"malihu", |
|||
"Malik", |
|||
"Masis", |
|||
"Minio", |
|||
"NGXS", |
|||
"NSWAG", |
|||
"Scriban", |
|||
"Serilog", |
|||
"Shoudly", |
|||
"Shouldly", |
|||
"Sweetalert", |
|||
"Syncfusion", |
|||
"Telerik", |
|||
"Timeago", |
|||
"Toastr", |
|||
"Volo", |
|||
"Volosoft", |
|||
"Xeevis" |
|||
], |
|||
"patterns": [ |
|||
{ |
|||
"name": "Markdown links", |
|||
"pattern": "\\((.*)\\)", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Markdown code blocks", |
|||
"pattern": "/^(\\s*`{3,}).*[\\s\\S]*?^\\1/gmx", |
|||
"description": "Taken from the cSpell example at https://cspell.org/configuration/patterns/#verbose-regular-expressions" |
|||
}, |
|||
{ |
|||
"name": "Inline code blocks", |
|||
"pattern": "\\`([^\\`\\r\\n]+?)\\`", |
|||
"description": "https://stackoverflow.com/questions/41274241/how-to-capture-inline-markdown-code-but-not-a-markdown-code-fence-with-regex" |
|||
}, |
|||
{ |
|||
"name": "Link contents", |
|||
"pattern": "\\<a(.*)\\>", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Snippet references", |
|||
"pattern": "-- snippet:(.*)", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Snippet references 2", |
|||
"pattern": "\\<\\[sample:(.*)", |
|||
"description": "another kind of snippet reference" |
|||
}, |
|||
{ |
|||
"name": "Multi-line code blocks", |
|||
"pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm" |
|||
}, |
|||
{ |
|||
"name": "HTML Tags", |
|||
"pattern": "<[^>]*>", |
|||
"description": "Reference: https://stackoverflow.com/questions/11229831/regular-expression-to-remove-html-tags-from-a-string" |
|||
}, |
|||
{ |
|||
"name": "Markdown Image", |
|||
"pattern": "!\\[(.*)\\]\\((.*)\\)" |
|||
} |
|||
], |
|||
"ignoreRegExpList": [ |
|||
"Markdown links", |
|||
"Markdown code blocks", |
|||
"Inline code blocks", |
|||
"Link contents", |
|||
"Snippet references", |
|||
"Snippet references 2", |
|||
"Multi-line code blocks", |
|||
"HTML Tags", |
|||
"Markdown Image" |
|||
], |
|||
"ignorePaths": [ |
|||
"**/*Release/Post.md", |
|||
"**/*Preview/POST.md" |
|||
] |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
# ABP.IO Platform 7.1 RC Has Been Released |
|||
|
|||
Today, we are happy to release the [ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) version **7.1 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. |
|||
|
|||
Try this version and provide feedback for a more stable version of ABP v7.1! Thanks to all of you. |
|||
|
|||
## Get Started with the 7.1 RC |
|||
|
|||
Follow the steps below to try version 7.1.0 RC today: |
|||
|
|||
1) **Upgrade** the ABP CLI to version `7.1.0-rc.1` using a command line terminal: |
|||
|
|||
````bash |
|||
dotnet tool update Volo.Abp.Cli -g --version 7.1.0-rc.1 |
|||
```` |
|||
|
|||
**or install** it if you haven't before: |
|||
|
|||
````bash |
|||
dotnet tool install Volo.Abp.Cli -g --version 7.1.0-rc.1 |
|||
```` |
|||
|
|||
2) Create a **new application** with the `--preview` option: |
|||
|
|||
````bash |
|||
abp new BookStore --preview |
|||
```` |
|||
|
|||
See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. |
|||
|
|||
> You can also use the [Get Started](https://abp.io/get-started) page to generate a CLI command to create a new application. |
|||
|
|||
You can use any IDE that supports .NET 7.x, like [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). |
|||
|
|||
## Migrating to 7.1 |
|||
|
|||
This version doesn't introduce any breaking changes. However, Entity Framework developers may need to add a new code-first database migration to their projects since we made some improvements to the existing entities of some application modules. |
|||
|
|||
## What's New with ABP Framework 7.1? |
|||
|
|||
In this section, I will introduce some major features released in this version. In addition to these features, so many enhancements have been made in this version too. |
|||
|
|||
Here is a brief list of the titles explained in the next sections: |
|||
|
|||
* Blazor WASM option added to Application Single Layer Startup Template |
|||
* Introducing the `IHasEntityVersion` interface and `EntitySynchronizer` base class |
|||
* Introducing the `DeleteDirectAsync` method for the `IRepository` interface |
|||
* Introducing the `IAbpHostEnvironment` interface |
|||
* Improvements on the eShopOnAbp project |
|||
* Others |
|||
|
|||
### Blazor WASM option added to Application Single Layer Startup Template |
|||
|
|||
We've created the [Application (Single Layer) Startup Template](https://docs.abp.io/en/abp/7.1/Startup-Templates/Application-Single-Layer) in v5.2 with three UI types: Angular, Blazor Server and MVC. At the moment, we didn't provide UI option for Blazor, because it required 3 projects at least (server-side, client-side and shared library between these two projects). |
|||
|
|||
In this version, we've added the Blazor WASM option to the **Application (Single Layer) Startup Template**. It still contains three projects (`blazor`, `host`, and `contracts`) but hosted by a single `host` project. |
|||
|
|||
You can use the following CLI command to create an `app-nolayers` template with the Blazor UI as the UI option: |
|||
|
|||
```bash |
|||
abp new TodoApp -t app-nolayers -u blazor --version 7.1.0-rc.1 |
|||
``` |
|||
|
|||
> You can check the [Quick Start documentation](https://docs.abp.io/en/abp/7.1/Tutorials/Todo/Single-Layer/Index?UI=Blazor&DB=EF) for a quick start with this template. |
|||
|
|||
### Introducing the `IHasEntityVersion` interface and `EntitySynchronizer` base class |
|||
|
|||
Entity synchronization is an important concept, especially in distributed applications and module development. If we have an entity that is related to other modules, we need to align/sync their data once the entity changes and versioning entity changes can also be good, so we can know whether they're synced or not. |
|||
|
|||
In this version, [@gdlcf88](https://github.com/gdlcf88) made a great contribution to the ABP Framework and introduced the `IHasEntityVersion` interface which adds **auto-versioning** to entity classes and `EntitySynchronizer` base class to **automatically sync an entity's properties from a source entity**. |
|||
|
|||
You can check the issue and documentation from the following links for more info: |
|||
|
|||
- [Issue: Entity synchronizers and a new EntityVersion audit property](https://github.com/abpframework/abp/issues/14196) |
|||
- [Versioning Entities](https://docs.abp.io/en/abp/7.1/Entities#versioning-entities) |
|||
- [Distributed Event Bus - Entity Synchronizer](https://docs.abp.io/en/abp/7.1/Distributed-Event-Bus#entity-synchronizer) |
|||
|
|||
> Note: The entities of some modules from the ABP Framework have implemented the `IHasEntityVersion` interface. Therefore, if you are upgrading your application from an earlier version, you need to create a new migration and apply it to your database. |
|||
|
|||
### Introducing the `DeleteDirectAsync` method for the `IRepository` interface |
|||
|
|||
EF 7 introduced a new [`ExecuteDeleteAsync`](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) method that deletes entities without involving the change tracker into the process. Therefore, it's much faster. |
|||
|
|||
We've added the `DeleteDirectAsync` method to the `IRepository<>` interface to take the full power of EF 7. It deletes all entities that fit the given predicate. It directly deletes entities from the database, without fetching them. Therefore, some features (like **soft-delete**, **multi-tenancy**, and **audit logging)** won't work, so use this method carefully when you need it. And use the `DeleteAsync` method if you need those features. |
|||
|
|||
### Introducing the `IAbpHostEnvironment` interface |
|||
|
|||
Sometimes, while creating an application, we need to get the current hosting environment and take actions according to that. In such cases, we can use some services such as [IWebHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-7.0) or [IWebAssemblyHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) provided by .NET, in the final application. |
|||
|
|||
However, we can not use these services in a class library, which is used by the final application. ABP Framework provides the `IAbpHostEnvironment` service, which allows you to get the current environment name whenever you want. `IAbpHostEnvironment` is used by the ABP Framework in several places to perform specific actions by the environment. For example, ABP Framework reduces the cache duration on the **Development** environment for some services. |
|||
|
|||
**Usage:** |
|||
|
|||
```csharp |
|||
public class MyService |
|||
{ |
|||
private readonly IAbpHostEnvironment _abpHostEnvironment; |
|||
|
|||
public MyService(IAbpHostEnvironment abpHostEnvironment) |
|||
{ |
|||
_abpHostEnvironment = abpHostEnvironment; |
|||
} |
|||
|
|||
public void MyMethod() |
|||
{ |
|||
//getting the current environment name |
|||
var environmentName = _abpHostEnvironment.EnvironmentName; |
|||
|
|||
//check for the current environment |
|||
if (_abpHostEnvironment.IsDevelopment()) { /* ... */ } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
You can inject the `IAbpHostEnvironment` into your service and get the current environment by using its `EnvironmentName` property. You can also check the current environment by using its extension methods such as `IsDevelopment()`. |
|||
|
|||
> Check the [ABP Application Startup](https://docs.abp.io/en/abp/7.1/Application-Startup) documentation for more information. |
|||
|
|||
### Improvements on the eShopOnAbp project |
|||
|
|||
K8s and Docker configurations have been made within this version (Dockerfiles and helm-charts have been added and image build scripts have been updated). See [#14083](https://github.com/abpframework/abp/issues/14083) for more information. |
|||
|
|||
### Others |
|||
|
|||
* Referral Links have been added to the CMS Kit Comment Feature (optional). You can specify common referral links (such as "nofollow" and "noreferrer") for links in the comments. See [#15458](https://github.com/abpframework/abp/issues/15458) for more information. |
|||
* ReCaptcha verification has been added to the CMS Kit Comment Feature (optional). You can enable ReCaptcha support to enable protection against bots. See the [documentation](https://docs.abp.io/en/abp/7.1/Modules/Cms-Kit/Comments) for more information. |
|||
* In the development environment, it is a must to reduce cache durations for some points. We typically don't have to invalidate the cache manually or wait on it for a certain time to be invalidated. For that purpose, we have reduced the cache durations for some points on the development environment. See [#14842](https://github.com/abpframework/abp/pull/14842) for more information. |
|||
|
|||
## What's New with ABP Commercial 7.1? |
|||
|
|||
We've also worked on [ABP Commercial](https://commercial.abp.io/) to align the new features and changes made in the ABP Framework. The following sections introduce a few new features coming with ABP Commercial 7.1. |
|||
|
|||
### Blazor WASM option added to Application Single Layer Pro Startup Template |
|||
|
|||
The [**Application (Single Layer) Startup Template**](https://docs.abp.io/en/commercial/latest/startup-templates/application-single-layer/index) with Blazor UI is also available for ABP Commercial customers with this version as explained above. |
|||
|
|||
You can use the following CLI command to create an `app-nolayers-pro` template with Blazor UI as the UI option: |
|||
|
|||
```bash |
|||
abp new TodoApp -t app-nolayers-pro -u blazor --version 7.1.0-rc.1 |
|||
``` |
|||
|
|||
You can also create an `app-nolayers-pro` template with Blazor UI via ABP Suite: |
|||
|
|||
 |
|||
|
|||
### Suite - MAUI Blazor Code Generation |
|||
|
|||
We provided a new UI option "MAUI Blazor" for the `app-pro` template in the previous version and it's possible to create a `maui-blazor` application with both ABP CLI and ABP Suite. |
|||
|
|||
You can create an `app-pro` template with the MAUI Blazor as the UI option with the following ABP CLI command: |
|||
|
|||
```bash |
|||
abp new Acme.BookStore -t app-pro -u maui-blazor |
|||
``` |
|||
|
|||
In this version, we implemented the code generation for MAUI Blazor. You can create and generate CRUD pages for this new UI option as you do in other UI types. |
|||
|
|||
> Note: MAUI Blazor is currently only available with the `app-pro` template. |
|||
|
|||
### SaaS Module - Allowing entering a username while impersonating the tenant |
|||
|
|||
In the previous versions, we were able to impersonate a tenant from the [SaaS Module's Tenant Management UI](https://docs.abp.io/en/commercial/7.1/modules/saas#tenant-management). There was a constraint in this approach, which forced us to only impersonate the "admin" user. However, the tenant might change the admin user's username, or we may want to impersonate another user of the tenant. |
|||
|
|||
Thus, with this version, we decided to allow the impersonation of the tenant by the specified username. |
|||
|
|||
*You can click on the "Login with this tenant" action button:* |
|||
|
|||
 |
|||
|
|||
*Then, Specify the admin name of the tenant:* |
|||
|
|||
 |
|||
|
|||
## Community News |
|||
|
|||
### New ABP Community Posts |
|||
|
|||
* [Sergei Gorlovetsky](https://community.abp.io/members/Sergei.Gorlovetsky) has created two new community articles: |
|||
* [Why ABP Framework is one of the best tools for migration from legacy MS Access systems to latest Web app](https://community.abp.io/posts/why-abp-framework-is-one-of-the-best-tools-for-migration-from-legacy-ms-access-systems-to-latest-web-app-7l39eof0) |
|||
* [ABP Framework — 5 steps Go No Go Decision Tree](https://community.abp.io/posts/abp-framework-5-steps-go-no-go-decision-tree-2sy6r2st) |
|||
* [Onur Pıçakcı](https://github.com/onurpicakci) has created his first ABP community article that explains how to contribute to ABP Framework. You can read it 👉 [here](https://community.abp.io/posts/how-to-contribute-to-abp-framework-46dvzzvj). |
|||
* [Maliming](https://github.com/maliming) has created a new community article to show how to convert create/edit modals to a page. You can read it 👉 [here](https://community.abp.io/posts/converting-createedit-modal-to-page-4ps5v60m). |
|||
|
|||
We thank you all. We thank all the authors for contributing to the [ABP Community platform](https://community.abp.io/). |
|||
|
|||
### Volosoft Attended NDC London 2023 |
|||
|
|||
 |
|||
|
|||
Core team members of the ABP Framework, [Halil Ibrahim Kalkan](https://twitter.com/hibrahimkalkan) and [Alper Ebicoglu](https://twitter.com/alperebicoglu) attended [NDC London 2023](https://ndclondon.com/) from the 23rd to the 27th of January. |
|||
|
|||
### Community Talks 2023.1: LeptonX Customization |
|||
|
|||
 |
|||
|
|||
In this episode of ABP Community Talks, 2023.1; we'll talk about **LeptonX Customization**. We will dive into the details and show you how to customize the [LeptonX Theme](https://leptontheme.com/) with examples. |
|||
|
|||
The event will be live on Thursday, February 16, 2023 (20:00 - 21:00 UTC). |
|||
|
|||
> Register to listen and ask your questions now 👉 https://kommunity.com/volosoft/events/abp-community-talks-20231-leptonx-customization-03f9fd8c. |
|||
|
|||
## Conclusion |
|||
|
|||
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://docs.abp.io/en/abp/7.1/Road-Map) documentation to learn about the release schedule and planned features for the next releases. Please try the ABP v7.1 RC and provide feedback to help us release a more stable version. |
|||
|
|||
Thanks for being a part of this community! |
|||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1,129 @@ |
|||
# How to add a custom grant type in OpenIddict |
|||
|
|||
## ITokenExtensionGrant |
|||
|
|||
Create a class that inherits `ITokenExtensionGrant`, and then register it with the framework. |
|||
|
|||
In the `MyTokenExtensionGrant` class below we try to get the token details, The `ForbidResult` handles the failure case and `SignInResult` returns a new token response, You can pass more parameters to implement business checks. |
|||
|
|||
```cs |
|||
public class MyTokenExtensionGrant : ITokenExtensionGrant |
|||
{ |
|||
public const string ExtensionGrantName = "MyTokenExtensionGrant"; |
|||
|
|||
public string Name => ExtensionGrantName; |
|||
public async Task<IActionResult> HandleAsync(ExtensionGrantContext context) |
|||
{ |
|||
var userToken = context.Request.GetParameter("token").ToString(); |
|||
|
|||
if (string.IsNullOrEmpty(userToken)) |
|||
{ |
|||
return new ForbidResult( |
|||
new[] {OpenIddictServerAspNetCoreDefaults.AuthenticationScheme}, |
|||
properties: new AuthenticationProperties(new Dictionary<string, string> |
|||
{ |
|||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest |
|||
}!)); |
|||
} |
|||
|
|||
var transaction = await context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerFactory>().CreateTransactionAsync(); |
|||
transaction.EndpointType = OpenIddictServerEndpointType.Introspection; |
|||
transaction.Request = new OpenIddictRequest |
|||
{ |
|||
ClientId = context.Request.ClientId, |
|||
ClientSecret = context.Request.ClientSecret, |
|||
Token = userToken |
|||
}; |
|||
|
|||
var notification = new OpenIddictServerEvents.ProcessAuthenticationContext(transaction); |
|||
var dispatcher = context.HttpContext.RequestServices.GetRequiredService<IOpenIddictServerDispatcher>(); |
|||
await dispatcher.DispatchAsync(notification); |
|||
|
|||
if (notification.IsRejected) |
|||
{ |
|||
return new ForbidResult( |
|||
new []{ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, |
|||
properties: new AuthenticationProperties(new Dictionary<string, string> |
|||
{ |
|||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, |
|||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, |
|||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri |
|||
})); |
|||
} |
|||
|
|||
var principal = notification.GenericTokenPrincipal; |
|||
if (principal == null) |
|||
{ |
|||
return new ForbidResult( |
|||
new []{ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, |
|||
properties: new AuthenticationProperties(new Dictionary<string, string> |
|||
{ |
|||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest, |
|||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription, |
|||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri |
|||
})); |
|||
} |
|||
|
|||
var userId = principal.FindUserId(); |
|||
var userManager = context.HttpContext.RequestServices.GetRequiredService<IdentityUserManager>(); |
|||
var user = await userManager.GetByIdAsync(userId.Value); |
|||
var userClaimsPrincipalFactory = context.HttpContext.RequestServices.GetRequiredService<IUserClaimsPrincipalFactory<IdentityUser>>(); |
|||
var claimsPrincipal = await userClaimsPrincipalFactory.CreateAsync(user); |
|||
claimsPrincipal.SetScopes(principal.GetScopes()); |
|||
claimsPrincipal.SetResources(await GetResourcesAsync(context, principal.GetScopes())); |
|||
await context.HttpContext.RequestServices.GetRequiredService<AbpOpenIddictClaimDestinationsManager>().SetAsync(principal); |
|||
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal); |
|||
} |
|||
|
|||
private async Task<IEnumerable<string>> GetResourcesAsync(ExtensionGrantContext context, ImmutableArray<string> scopes) |
|||
{ |
|||
var resources = new List<string>(); |
|||
if (!scopes.Any()) |
|||
{ |
|||
return resources; |
|||
} |
|||
|
|||
await foreach (var resource in context.HttpContext.RequestServices.GetRequiredService<IOpenIddictScopeManager>().ListResourcesAsync(scopes)) |
|||
{ |
|||
resources.Add(resource); |
|||
} |
|||
return resources; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```cs |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
PreConfigure<OpenIddictServerBuilder>(builder => |
|||
{ |
|||
builder.Configure(openIddictServerOptions => |
|||
{ |
|||
openIddictServerOptions.GrantTypes.Add(MyTokenExtensionGrant.ExtensionGrantName); |
|||
}); |
|||
}); |
|||
//... |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
Configure<AbpOpenIddictExtensionGrantsOptions>(options => |
|||
{ |
|||
options.Grants.Add(MyTokenExtensionGrant.ExtensionGrantName, new MyTokenExtensionGrant()); |
|||
}); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
## Source code |
|||
|
|||
https://github.com/abpframework/abp/commit/3210f138454697647689b4868c8d4b7b3da02d44 |
|||
|
|||
|
|||
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 425 KiB |
@ -0,0 +1,47 @@ |
|||
# The new EF Core interceptors |
|||
|
|||
## Interceptors |
|||
|
|||
EF Core 7 has made a lot of enhancements to interceptors, You can see the list from [EF Core improved interceptors](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#new-and-improved-interceptors-and-events). |
|||
|
|||
* Interception for creating and populating new entity instances (aka "materialization") |
|||
* Interception to modify the LINQ expression tree before a query is compiled |
|||
* Interception for optimistic concurrency handling (DbUpdateConcurrencyException) |
|||
* Interception for connections before checking if the connection string has been set |
|||
* Interception for when EF Core has finished consuming a result set, but before that result set is closed |
|||
* Interception for the creation of a DbConnection by EF Core |
|||
* Interception for DbCommand after it has been initialized |
|||
|
|||
## Lazy initialization of `connection string` |
|||
|
|||
You generally don't need to use [this](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#lazy-initialization-of-a-connection-string) feature, ABP has its own [connection string feature](https://docs.abp.io/en/abp/latest/Connection-Strings). |
|||
|
|||
The framework will automatically handle the module or multi-tenant connection string |
|||
|
|||
## Add interceptors in `AbpDbContext` |
|||
|
|||
[Add interceptors](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#registering-interceptors) is very simple, Add your `interceptors` in the `OnConfiguring` method of `DbContext` |
|||
|
|||
````csharp |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>, |
|||
{ |
|||
|
|||
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |
|||
{ |
|||
optionsBuilder.AddInterceptors(new MyEfCorenterceptor()); |
|||
|
|||
base.OnConfiguring(optionsBuilder); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> Some interceptors may be [Singleton](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0#service-lifetimes) services. This means a single instance is used by many `DbContext` instances. The implementation must be thread-safe. |
|||
|
|||
|
|||
See the [EF Core Interceptors documentation](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors) for more information. |
|||
@ -0,0 +1,197 @@ |
|||
# gRPC - JSON Transcoding |
|||
|
|||
In this article, I'll show you one of the new features that came with .NET 7: **JSON Transcoding**. |
|||
|
|||
> I've created a community article to highlight some interesting features (briefly) that are now available with the release of .NET 7. You can check it from [here](https://community.abp.io/posts/whats-new-with-.net-7-tlq2g43w). |
|||
|
|||
## What is gRPC? What are the pros and cons? |
|||
|
|||
[gRPC](https://grpc.io/) is a high-performance RPC framework and uses HTTP/2 and Protobuf (protocol buffers). |
|||
|
|||
### gRPC - Advantages |
|||
|
|||
* **Lightweight messages**: Payload is much smaller than JSON. |
|||
* **High performance**: Much faster than REST and JSON communication. |
|||
* **Faster serialization/deserialization**: Protobufs are binary encoded. This means that it will use fewer CPU cycles to serialize/deserialize the messages. |
|||
|
|||
### gRPC - Disadvantages |
|||
|
|||
* **Lack of maturity** |
|||
* **Limited browser support - hard to test (in a way)**: Since gRPC relies on HTTP/2, you can’t call a gRPC service from a web browser directly. This is where JSON Transcoding comes into play. |
|||
|
|||
## What is JSON Transcoding? |
|||
|
|||
Despite the benefits that gRPC provides, REST APIs have an important place in modern web applications. We can easily create, expose and test them. |
|||
|
|||
On the other hand, gRPC services are not easy to test as mentioned above. We can’t access a gRPC service yet through the browser since browsers don’t understand binary protocols. We need to use some tools to access endpoints, such as [gRPCurl](https://github.com/fullstorydev/grpcurl). |
|||
|
|||
 |
|||
|
|||
JSON Transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs for gRPC services and overcomes this problem. You can see an illustration down below: |
|||
|
|||
 |
|||
|
|||
By using JSON Transcoding, we can expose our gRPC services and access them via HTTP call. Let's see it in action. |
|||
|
|||
## Creating & Exposing gRPC Services in ABP Based Applications |
|||
|
|||
> You can find the source code of the application at https://github.com/EngincanV/abp-grpc-json-transcoding. |
|||
|
|||
### Creating the Application |
|||
|
|||
Create an empty folder, open a command-line terminal and type the following command in the terminal window to create a new ABP solution using the ABP CLI: |
|||
|
|||
```csharp |
|||
abp new BookStore -t app --ui angular --preview |
|||
``` |
|||
|
|||
I've created an application with the Angular UI, but the UI is not important for this tutorial, you can select your favorite UI. |
|||
|
|||
> **Note:** Your application should be ABP 7.0+. |
|||
|
|||
### Configuring gRPC & JSON Transcoding |
|||
|
|||
In this solution, `*.HttpApi.Host` is the project that configures and runs the server-side application. So, we will make changes in that project. |
|||
|
|||
First, we need to add two packages ([Grpc.AspNetCore](https://www.nuget.org/packages/Grpc.AspNetCore) and [Microsoft.AspNetCore.Grpc.JsonTranscoding](https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.JsonTranscoding)) into this project with the following commands: |
|||
|
|||
```bash |
|||
dotnet add package Grpc.AspNetCore |
|||
dotnet add package Microsoft.AspNetCore.Grpc.JsonTranscoding |
|||
``` |
|||
|
|||
Then, add the [*google/api/http.proto*](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/http.proto) and [*google/api/annotations.proto*](https://github.com/dotnet/aspnetcore/blob/main/src/Grpc/JsonTranscoding/test/testassets/Sandbox/google/api/annotations.proto) files under to the **google/api** folder. These files are required for JSON Transcoding and contain all the stuff related to that. |
|||
|
|||
 |
|||
|
|||
Then, open the module class (`BookStoreHttpApiHostModule.cs`) and update it as below: |
|||
|
|||
```csharp |
|||
public class BookStoreHttpApiHostModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
// other configurations... |
|||
|
|||
context.Services.AddGrpc().AddJsonTranscoding(); //add this line |
|||
|
|||
} |
|||
|
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var app = context.GetApplicationBuilder(); |
|||
|
|||
// other middlewares... |
|||
|
|||
app.UseConfiguredEndpoints(builder => |
|||
{ |
|||
builder.MapGrpcService<BookAppService>(); //add this line |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* Here, we've registered the related gRPC services into the DI container by calling the `AddGdpr()` and `AddJsonTranscoding()` methods. |
|||
* Also, we've defined our gRPC service: `BookAppService`. We have not created this service yet and will create it in the next section. |
|||
|
|||
So far, we've added the required packages to our project and made the related configurations. Now we can start creating our gRPC services. |
|||
|
|||
### Implementing gRPC Services |
|||
|
|||
Create a **Protos** folder and define a `bookstore.proto` file in it. Then fill it with the below content: |
|||
|
|||
```proto |
|||
syntax = "proto3"; |
|||
|
|||
import "google/api/annotations.proto"; //import related Google APIs |
|||
|
|||
option csharp_namespace = "BookStore"; |
|||
|
|||
message Book { |
|||
string title = 1; |
|||
string author = 2; |
|||
int32 page_count = 3; |
|||
optional string language = 4; |
|||
} |
|||
|
|||
message GetBookListRequest {} |
|||
|
|||
message GetBookListResponse { repeated Book books = 1; } |
|||
|
|||
service BookApp { |
|||
rpc GetBookList(GetBookListRequest) returns (GetBookListResponse) { |
|||
option (google.api.http) = { |
|||
get: "/v1/book-store/books" //expose from this url |
|||
}; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* Here, we've defined the `bookstore.proto` file. You can consider this file as a contract that the server and client have agreed on. |
|||
* An important point here, we've imported the `google/api/annotations.proto` (we added this file under the **google/api** folder in the previous section) and by using the `google.api.http` we are exposing this gRPC service (with the specified URL). |
|||
* So, when we run the application we can send a request to that endpoint and see the result. |
|||
* Thanks to JSON Transcoding, we don't need to use any other tools to test our gRPC services anymore! |
|||
|
|||
Let's mark the `bookstore.proto` file as our protobuf file in the `*.HttpApi.Host.csproj` file: |
|||
|
|||
```xml |
|||
<ItemGroup> |
|||
<Protobuf Include="Protos\bookstore.proto" GrpcServices="Server" /> |
|||
</ItemGroup> |
|||
``` |
|||
|
|||
For the final step, we just need to define a service that corresponds to the service that we've defined in the `bookstore.proto` file. Voilà 🎉! |
|||
|
|||
So, create a new class named `BookAppService` under the `Services` folder: |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Grpc.Core; |
|||
|
|||
namespace BookStore.Services; |
|||
|
|||
public class BookAppService : BookApp.BookAppBase |
|||
{ |
|||
//notice: we did not create GetBookListResponse and GetBookListRequest classes |
|||
//Grpc.AspNetCore package did behalf of us |
|||
public override Task<GetBookListResponse> GetBookList(GetBookListRequest request, ServerCallContext context) |
|||
{ |
|||
var response = new GetBookListResponse(); |
|||
response.Books.Add(new Book |
|||
{ |
|||
Title = "The Hitchhiker's Guide to the Galaxy", |
|||
Author = "Douglas Adams", |
|||
PageCount = 42 |
|||
}); |
|||
response.Books.Add(new Book |
|||
{ |
|||
Title = "The Lord of the Rings", |
|||
Author = "J.R.R. Tolkien", |
|||
PageCount = 1234 |
|||
}); |
|||
|
|||
return Task.FromResult(response); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* Here, as you notice we've inherited our service from the `BookAppBase` class. You might wonder where it came from. |
|||
* The **Grpc.AspNetCore** package creates auto-generated classes from our protobuf file (`bookstore.proto`). We just need to inherit from `BookAppService` class (it's matched with the service name that we've defined in protobuf file), override the `GetBookList` method and implement it. That's it. |
|||
* Also, notice we did not create the other classes such as `Book`, `GetBookListRequest` and `GetBookListResponse`. These are all auto-generated from our protobuf file. |
|||
|
|||
We've created a gRPC service and now let's run our application (before running the application, run the `*.DbMigrator` project to create the database and seed the initial data) and see it in action. |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I've briefly introduced the JSON Transcoding feature that was shipped with .NET 7 and showed it in an ABP Based application. |
|||
|
|||
> See the [gRPC JSON transcoding in ASP.NET Core gRPC apps](https://learn.microsoft.com/en-us/aspnet/core/grpc/json-transcoding?view=aspnetcore-7.0) documentation for more information. |
|||
|
|||
## References |
|||
|
|||
* https://devblogs.microsoft.com/dotnet/announcing-grpc-json-transcoding-for-dotnet/ |
|||
* https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0#json-transcoding |
|||
* https://learn.microsoft.com/en-us/aspnet/core/grpc/json-transcoding?view=aspnetcore-7.0 |
|||
* https://sahansera.dev/building-grpc-server-dotnet/ |
|||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 534 KiB |
@ -0,0 +1,71 @@ |
|||
# Signalr Client results |
|||
|
|||
ASP.NET Core 7 supports [requesting a result from a client](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0#signalr), in this article, we will show you how to use client results with the ABP framework. |
|||
|
|||
## Create a SignalR hub |
|||
|
|||
```csharp |
|||
public class ChatHub : AbpHub |
|||
{ |
|||
public async Task<string> WaitForMessage(string connectionId) |
|||
{ |
|||
var message = await Clients.Client(connectionId).InvokeAsync<string>("GetMessage"); |
|||
return message; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* ChatHub inherits from `AbpHub` that has useful base properties like `CurrentUser`. |
|||
* Define the `WaitForMessage` method to call the client's `GetMessage` method and get the return value. |
|||
|
|||
> Using `InvokeAsync` from a Hub method requires setting the [MaximumParallelInvocationsPerClient](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-7.0&tabs=dotnet#configure-server-options) option to a value greater than 1. |
|||
|
|||
## Client |
|||
|
|||
Clients should return results in their `.On(...)` handlers. |
|||
|
|||
### .NET Client |
|||
|
|||
```csharp |
|||
hubConnection.On("GetMessage", async () => |
|||
{ |
|||
Console.WriteLine("Enter message:"); |
|||
var message = await Console.In.ReadLineAsync(); |
|||
return message; |
|||
}); |
|||
``` |
|||
|
|||
### JavaScript client |
|||
|
|||
```js |
|||
connection.on("GetMessage", function () { |
|||
|
|||
const message = prompt("Enter message:"); |
|||
return message; |
|||
}); |
|||
``` |
|||
|
|||
## Strongly-typed hubs |
|||
|
|||
We can use strongly-typed instead of `InvokeAsync` by inheriting from `AbpHub<T>` or `Hub<T>`: |
|||
|
|||
```csharp |
|||
public interface IClient |
|||
{ |
|||
Task<string> GetMessage(); |
|||
} |
|||
|
|||
public class ChatHub : AbpHub<IClient> |
|||
{ |
|||
public async Task<string> WaitForMessage(string connectionId) |
|||
{ |
|||
string message = await Clients.Client(connectionId).GetMessage(); |
|||
return message; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## See also: |
|||
|
|||
* [ABP SignalR-Integration documentation](https://docs.abp.io/en/abp/latest/SignalR-Integration) |
|||
* [Microsoft client-results documentation](https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-7.0#client-results) |
|||
@ -0,0 +1,139 @@ |
|||
# Inheritance Strategies in Entity Framework Core 7 |
|||
|
|||
In this article, I'll show you all the object mapping strategies of EF Core and especially the new one: **TPC Inheritance Mapping**. TPC mapping is the new feature introduced with Entity Framework Core 7. It's called Table Per Concrete type inheritance mapping. I'll explain the EF Core's inheritance mapping strategies with how the inherited entity and the specific information are saved into a relational database. I'll also explain which strategy is best for your case. |
|||
|
|||
By default, EF Core maps an inheritance hierarchy of .NET types to a single database table. And it's called TPH (table-per-hierarchy) mapping. |
|||
|
|||
In the previous versions EF Core , Table Per Hierarchy (TPH) or Table Per Type (TPT) were supported. And now TPC came! |
|||
|
|||
To explain in a more clear way, I'll use the following entity model. |
|||
`Car`, `Bus` and `Motorcylce` are inherited from the `Vehicle` object. |
|||
|
|||
```csharp |
|||
public abstract class Vehicle |
|||
{ |
|||
public int Id { get; set; } |
|||
public string VehicleModel { get; set; } |
|||
} |
|||
|
|||
////////////////////////////////////// |
|||
|
|||
public class Car : Vehicle |
|||
{ |
|||
public string Segment { get; set; } |
|||
} |
|||
|
|||
////////////////////////////////////// |
|||
|
|||
public class Bus : Vehicle |
|||
{ |
|||
public int? SeatCount { get; set; } |
|||
} |
|||
|
|||
////////////////////////////////////// |
|||
|
|||
public class Motorcycle : Vehicle |
|||
{ |
|||
public int CylinderCount { get; set; } |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
Here's the list of available strategies to map `Car`, `Bus` and `Motorcylce`: |
|||
|
|||
## TPH (Table Per Hierarchy) |
|||
|
|||
TPH maps an inheritance hierarchy of .NET types to **a single database table**. So a single table is created for all types. This is the default behavior of EF Core. If you explicitly want to use this strategy, you can write `UseTphMappingStrategy()` to the root entity in the `OnModelCreating()` method in the `DbContext` class. As you see from the following table, it adds an extra column, `Discriminator` to separate the `Car`, `Bus` and `Motorcycle`. This way of persistence is an unnormalized form. |
|||
|
|||
```sql |
|||
CREATE TABLE [Vehicles] ( |
|||
[Id] int NOT NULL IDENTITY, |
|||
[Discriminator] nvarchar(max) NOT NULL, -- "Car", "Bus", "Motorcylce" |
|||
[VehicleModel] nvarchar(max) NOT NULL, |
|||
[Segment] nvarchar(max) NULL, |
|||
[SeatCount] int NULL, |
|||
[CylinderCount] int NOT NULL |
|||
); |
|||
``` |
|||
|
|||
## TPT (Table per Type) |
|||
|
|||
With the TPT strategy, a different table is created for every type. The table itself is used to determine the type of the object saved, and each table contains only columns for the properties of that type. The data is saved in multiple tables, and this way of persistence is normalized form. |
|||
|
|||
```sql |
|||
CREATE TABLE [Vehicles] ( |
|||
[Id] int NOT NULL IDENTITY, |
|||
[VehicleModel] nvarchar(max) NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE [Car] ( |
|||
[Id] int NOT NULL, |
|||
[Segment] nvarchar(max) NULL |
|||
); |
|||
|
|||
CREATE TABLE [Bus] ( |
|||
[Id] int NOT NULL, |
|||
[SeatCount] int NULL, |
|||
); |
|||
|
|||
CREATE TABLE [Motorcycle] ( |
|||
[Id] int NOT NULL, |
|||
[CylinderCount] int NOT NULL |
|||
); |
|||
``` |
|||
|
|||
If you explicitly want to use this strategy, you can write `UseTptMappingStrategy()` to the root entity in the `OnModelCreating()` method in the `DbContext` class. |
|||
|
|||
## TPC (Table Per Concrete type) |
|||
|
|||
It's the new strategy that has been introduced with EF Core 7. In this way, a different table is created for each concrete type. While in the TPT, the table itself indicates the type of the object saved, in TPC, each table contains columns for every property in the concrete type *and its base types*. As you see from the following tables, no `Vehicles` table is being created. The `VehicleModel` field is common in each table. |
|||
|
|||
|
|||
|
|||
```sql |
|||
CREATE TABLE [Car] ( |
|||
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [VehicleIds]), |
|||
[VehicleModel] nvarchar(max) NOT NULL, -- common field for each type |
|||
[Segment] nvarchar(max) NULL |
|||
); |
|||
|
|||
CREATE TABLE [Bus] ( |
|||
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [VehicleIds]), |
|||
[VehicleModel] nvarchar(max) NOT NULL, -- common field for each type |
|||
[SeatCount] int NULL |
|||
); |
|||
|
|||
CREATE TABLE [Motorcycle] ( |
|||
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [VehicleIds]), |
|||
[VehicleModel] nvarchar(max) NOT NULL, -- common field for each type |
|||
[CylinderCount] int NOT NULL |
|||
); |
|||
``` |
|||
|
|||
This strategy maps each .NET type to **a different database table**. You write `UseTpcMappingStrategy()` to the root entity in the root entity in the `OnModelCreating()` method in the `DbContext` class. |
|||
|
|||
|
|||
|
|||
## How do you choose which mapping type is best for you? |
|||
|
|||
* **TPH:** In most cases, the TPH mapping is a good choice which is the default one. This way, the table columns increase because all the properties are saved in the same table, but it's easy to query in a single table. |
|||
|
|||
* **TPT** In this form, data is persisted in a normalized form. For this reason, you can choose TPT, but generally, it's not being used. But the disadvantage is when you filter by `VehicleModel` column, it must join `Vehicles`, `Bus`, `Car`, `Motorcycle` tables. So it's not a feasible fetching way. |
|||
* **TPC:** This new strategy is similar to TPT but resolves some of the TPT problems. Because it stores the data in its own table, it doesn't divide across multiple tables. This option can be chosen when the mapped hierarchy is large and has many concrete types, each with many properties. |
|||
|
|||
For example, if `Car`, `Bus`, or `Motorcycle` have 20 properties for each type, then it's better to store them in a separate table, so we should use TPC. But if these entities have only 3-5 properties, then TPH is the way to go. |
|||
|
|||
|
|||
|
|||
> ### Jeremy Likness says; |
|||
> |
|||
> - If your code mostly queries entities of a single leaf type, then use TPC because the storage requirements are smaller since there are no null columns and no discriminator no index is ever needed on the discriminator column, which would slow down updates and possibly also queries. An index may not be needed when using TPH either, but that depends on various factors. If your code mostly queries for entities of many types, such as writing queries against the base type, lean towards TPH. If your database system supports it (for example SQL Server), then consider using sparse columns for columns that will be rarely populated. Use TPT only if constrained to do so by external factors. |
|||
|
|||
|
|||
|
|||
*References:* |
|||
|
|||
* https://devblogs.microsoft.com/dotnet/announcing-ef7-preview5/#primary-keys |
|||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,134 @@ |
|||
# JSON Columns in Entity Framework Core 7 |
|||
|
|||
In this article, we will see how to use the new **JSON Columns** features that came with EF Core 7 in an ABP based application (with examples). |
|||
|
|||
## JSON Columns |
|||
|
|||
Most relational databases support columns that contain JSON documents. The JSON in these columns can be drilled into with queries. This allows, for example, filtering and sorting by the elements of the documents, as well as projection of elements out of the documents into results. JSON columns allow relational databases to take on some of the characteristics of document databases, creating a useful hybrid between these two database management approaches. |
|||
|
|||
EF7 contains provider-agnostic support for JSON columns, with an implementation for SQL Server. This support allows the mapping of aggregates built from .NET types to JSON documents. Normal LINQ queries can be used on the aggregates, and these will be translated to the appropriate query constructs needed to drill into the JSON. EF7 also supports updating and saving changes to JSON documents. |
|||
|
|||
> You can find more information about JSON columns in EF Core's [documentation](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns). |
|||
|
|||
### Mapping JSON Columns |
|||
|
|||
In EF Core, aggregate types can be defined using `OwnsOne` and `OwnsMany` methods. `OwnsOne` can be used to map a single aggregate and the `OwnsMany` method can be used to map a collection of aggregates. |
|||
|
|||
With EF 7, we have a new extension method for mapping property to a JSON Column: `ToJson`. We can use this method to mark a property as a JSON Column. The property can be of any type that can be serialized to JSON. |
|||
|
|||
The following example shows how to map a JSON column to an aggregate type: |
|||
|
|||
```csharp |
|||
public class ContactDetails |
|||
{ |
|||
public Address Address { get; set; } |
|||
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 class Person : AggregateRoot<int> |
|||
{ |
|||
public string Name { get; set; } = null!; |
|||
public ContactDetails ContactDetails { get; set; } = null!; |
|||
} |
|||
``` |
|||
|
|||
* Above, we have defined an aggregate type `ContactDetails` that contains an `Address` and a `Phone` number. The aggregate type is configured in `OnModelCreating` using `OwnsOne` and `ToJson` methods below. |
|||
* The `Address` property is mapped to a JSON column using `ToJson`, and the `Phone` property is mapped to a regular column. This requires just one call to **ToJson()** when configuring the aggregate type: |
|||
|
|||
```csharp |
|||
|
|||
public class MyDbContext : AbpDbContext<MyDbContext> |
|||
{ |
|||
public DbSet<Person> Persons { get; set; } |
|||
|
|||
public MyDbContext(DbContextOptions<MyDbContext> options) |
|||
: base(options) |
|||
{ |
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
builder.Entity<Person>(b => |
|||
{ |
|||
b.ToTable(MyProjectConsts.DbTablePrefix + "Persons", MyProjectConsts.DbSchema); |
|||
b.ConfigureByConvention(); |
|||
b.OwnsOne(x=>x.ContactDetails, c => |
|||
{ |
|||
c.ToJson(); //mark as JSON Column |
|||
c.OwnsOne(cd => cd.Address); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Querying JSON Columns |
|||
|
|||
Queries into JSON columns work just the same as querying into any other aggregate type in EF Core. That's it, just use the LINQ! Here are some examples: |
|||
|
|||
```csharp |
|||
var persons = await (await GetDbSetAsync()).ToListAsync(); |
|||
|
|||
var contacts = await (await GetDbSetAsync()).Select(person => new |
|||
{ |
|||
person, |
|||
person.ContactDetails.Phone, //query over JSON column |
|||
Addresses = person.ContactDetails.Address //query over JSON column |
|||
}).ToListAsync(); |
|||
|
|||
var addresses = await (await GetDbSetAsync()).Select(person => new |
|||
{ |
|||
person, |
|||
Addresses = person.ContactDetails.Address //query over JSON column |
|||
}).ToListAsync(); |
|||
``` |
|||
|
|||
### Updating JSON Columns |
|||
|
|||
You can update JSON columns the same as updating any record by using the `UpdateAsync` method. The following example shows how to update a JSON column: |
|||
|
|||
```csharp |
|||
var person = await (await GetDbSetAsync()).FirstAsync(); |
|||
|
|||
person.ContactDetails.Phone = "123456789"; |
|||
person.ContactDetails.Address = new Address("Street", "City", "Postcode", "Country"); |
|||
await UpdateAsync(person, true); |
|||
``` |
|||
|
|||
### JSON Column in a Database |
|||
|
|||
After you've configured the database relations, created a new migration and applied it to database you will have a database table like below: |
|||
|
|||
 |
|||
|
|||
As you can see, thanks to JSON Columns feature the **ContactDetails** row has JSON content and we can use it in a query or update it from our application with the LINQ JSON query support that mentioned above. |
|||
|
|||
### Conclusion |
|||
|
|||
In this article, I've briefly introduced the JSON Columns feature that was shipped with EF Core 7. It's pretty straightforward to use JSON Columns in an ABP based application. You can see the examples above and give it a try! |
|||
|
|||
### The Source Code |
|||
* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreJSONColumnDemo). |
|||
|
|||
### References |
|||
|
|||
* [https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns) |
|||
* [https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities](https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities) |
|||
@ -0,0 +1,73 @@ |
|||
# gRPC - Health Checks |
|||
|
|||
In this article we will show how to use gRPC health checks with the ABP Framework. |
|||
|
|||
## Health Checks |
|||
|
|||
ASP.NET Core 7 supports gRPC health checks. Health Checks allow us to determine the overall health and availability of our application infrastructure. They are exposed as HTTP endpoints and can be configured to provide information for various monitoring scenarios, such as the response time and memory usage of our application, or whether our application can communicate with our database provider. |
|||
|
|||
### gRPC Health Checks |
|||
|
|||
The [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) is a standard for reporting the health of gRPC server apps. An app exposes health checks as a gRPC service. They are typically used with an external monitoring service to check the status of an app. |
|||
|
|||
### Grpc.AspNetCore.HealthChecks |
|||
|
|||
ASP.NET Core supports the gRPC health checking protocol with the [Grpc.AspNetCore.HealthChecks](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks) package. Results from .NET health checks are reported to callers. |
|||
|
|||
## Using gRPC Health Checks with the ABP Framework |
|||
|
|||
In this article, I'm assuming you've used gRPC with ABP before. If you are still having problems with this, it may be good for you to review this article. |
|||
https://community.abp.io/posts/using-grpc-with-the-abp-framework-2dgaxzw3 |
|||
|
|||
### Set up gRPC Health Checks |
|||
|
|||
In this solution, `*.HttpApi.Host` is the project that configures and runs the server-side application. So, we will make changes in that project. |
|||
|
|||
* Add the `Grpc.AspNetCore.HealthChecks` package to your project. |
|||
|
|||
```bash |
|||
dotnet add package Grpc.AspNetCore.HealthChecks |
|||
``` |
|||
|
|||
* `AddGrpcHealthChecks` to register services that enable health checks. |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
// Other configurations... |
|||
|
|||
context.Services.AddGrpcHealthChecks() |
|||
.AddCheck("SampleHealthCheck", () => HealthCheckResult.Healthy()); |
|||
} |
|||
``` |
|||
* `MapGrpcHealthChecksService` to add a health check service endpoint. |
|||
|
|||
```csharp |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
// Other middlewares... |
|||
|
|||
app.UseConfiguredEndpoints(builder => |
|||
{ |
|||
builder.MapGrpcHealthChecksService(); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### Calling Health Checks From a Client |
|||
|
|||
Now that our server is configured for gRPC health checks, we can test it by creating a basic console client. |
|||
|
|||
```csharp |
|||
var channel = GrpcChannel.ForAddress("https://localhost:44357"); |
|||
var client = new Health.HealthClient(channel); |
|||
|
|||
var response = await client.CheckAsync(new HealthCheckRequest()); |
|||
var status = response.Status; |
|||
|
|||
Console.WriteLine($"Health Status: {status}"); |
|||
``` |
|||
|
|||
## References |
|||
|
|||
- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0#grpc-health-checks-in-aspnet-core |
|||
@ -0,0 +1,88 @@ |
|||
# Model building conventions in Entity Framework Core 7.0 |
|||
|
|||
In this article, I will show you one of the new features of EF Core 7 named "Model building conventions". |
|||
|
|||
Entity Framework Core uses a metadata model to describe how entity types are mapped to the database. Before EF Core 7.0, it was not possible to remove or replace existing conventions or add new conventions. With EF Core 7.0, this is now possible. To read more about it, you can visit its [documentation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#model-building-conventions). |
|||
|
|||
EF Core uses many built-in conventions. You can see the full list of the conventions on `IConvetion` Interface API [documentation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.conventions.iconvention?view=efcore-7.0). |
|||
|
|||
If you want to add, remove or replace a convention, you need to override `ConfigureConventions` method of your DbContext as shown below; |
|||
|
|||
```csharp |
|||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) |
|||
{ |
|||
configurationBuilder.Conventions.Add(_ => new MyCustomConvention()); |
|||
} |
|||
``` |
|||
|
|||
## Allowed Operations |
|||
|
|||
### Removing an existing convention |
|||
|
|||
Existing conventions provided by EF Core are well thought and useful, but sometimes some of them might not be a good candidate for your application. In such cases, you can remove an existing convention as shown below; |
|||
|
|||
```csharp |
|||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) |
|||
{ |
|||
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention)); |
|||
} |
|||
``` |
|||
|
|||
### Adding a new convention |
|||
|
|||
Just like removing a convention, we can add a completely new convention as well. You can define many different conventions here. For example, you can specify a standard precision for all decimal fields in your entities. |
|||
|
|||
```csharp |
|||
public class DecimalPrecisionConvention : IModelFinalizingConvention |
|||
{ |
|||
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context) |
|||
{ |
|||
foreach (var property in modelBuilder.Metadata.GetEntityTypes() |
|||
.SelectMany( |
|||
entityType => entityType.GetDeclaredProperties() |
|||
.Where( |
|||
property => property.ClrType == typeof(decimal)))) |
|||
{ |
|||
property.Builder.HasPrecision(2); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Note that, conventions are executed in the order they are added. So you need to be careful in which order they are added. |
|||
|
|||
### Replacing an existing convention |
|||
|
|||
Sometimes, a default convention might work slightly different than what your app expects. In such cases, you can create your own implementation by inheriting from that convention and replace the default one. For example, you can create a convention as shown below; |
|||
|
|||
```csharp |
|||
public class MyCustomConvention : ADefaultEfCoreConvention |
|||
{ |
|||
public MyCustomConvention(ProviderConventionSetBuilderDependencies dependencies) |
|||
: base(dependencies) |
|||
{ |
|||
} |
|||
// override the methods you want to change. |
|||
} |
|||
``` |
|||
|
|||
Then, you can replace the default one with your implementation as shown below; |
|||
|
|||
```csharp |
|||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) |
|||
{ |
|||
configurationBuilder.Conventions.Replace<ADefaultEfCoreConvention>( |
|||
serviceProvider => new MyCustomConvention( |
|||
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>())); |
|||
} |
|||
``` |
|||
|
|||
As a final note, conventions never override configuration marked as **DataAnnotation** or **Explicit**. This means that, even if there is a convention, if the property has a `DataAnnotation` attribute or configuration in `OnModelCreating`, convetion will not be used. Here are the configuration types EF Core uses; |
|||
|
|||
* **Explicit:** The model element was explicitly configured in OnModelCreating |
|||
* **DataAnnotation:** The model element was configured using a mapping attribute (aka data annotation) on the CLR type |
|||
* **Convention:** The model element was configured by a model building convention |
|||
|
|||
## Using in ABP-based solution |
|||
|
|||
Since ABP uses EF Core, you can use this feature in ABP as well. |
|||
@ -0,0 +1,71 @@ |
|||
# Bulk Operations with Entity Framework Core 7.0 |
|||
Entity Framework tracks all the entity changes and applies those changes to the database one by one when the `SaveChanges()` method is called. There was no way to execute bulk operations in Entity Framework Core without a dependency. |
|||
|
|||
As you know the [Entity Framework Extensions](https://entityframework-extensions.net/bulk-savechanges) library was doing it but it was not free. |
|||
|
|||
There was no other solution until now. [Bulk Operations](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) are now available in Entity Framework Core with .NET 7. |
|||
|
|||
With .NET 7, there are two new methods such as `ExecuteUpdate` and `ExecuteDelete` available to execute bulk operations. It's a similar usage with the Entity Framework Core Extensions library if you're familiar with it. |
|||
|
|||
You can visit the microsoft example [here](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) about how to use it. |
|||
|
|||
|
|||
It can be easily used with the DbContext. |
|||
|
|||
```csharp |
|||
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync(); |
|||
``` |
|||
|
|||
## Using with ABP Framework |
|||
ABP Framework provides an abstraction over database operations and implements generic repository pattern. So, DbContext can't be accessed outside of [repositories](https://docs.abp.io/en/abp/latest/Repositories). |
|||
|
|||
You can use the `ExecuteUpdate` and `ExecuteDelete` methods inside a repository. |
|||
|
|||
```csharp |
|||
public class BookEntityFrameworkCoreRepository : EfCoreRepository<BookStoreDbContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public BookEntityFrameworkCoreRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider) : base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task UpdateListingAsync() |
|||
{ |
|||
var dbSet = await GetDbSetAsync(); |
|||
|
|||
await dbSet |
|||
.Where(x => x.IsListed && x.PublishedOn.Year <= 2022) |
|||
.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsListed, x => false)); |
|||
} |
|||
|
|||
public async Task DeleteOldBooksAsync() |
|||
{ |
|||
var dbSet = await GetDbSetAsync(); |
|||
|
|||
await dbSet |
|||
.Where(x => x.PublishedOn.Year <= 2000) |
|||
.ExecuteDeleteAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
There is no need to take an action for bulk inserting. You can use the `InsertManyAsync` method of the repository instead of creating a new method for it if you don't have custom logic. It'll use a new bulk inserting feature automatically since it's available in EF Core 7.0. |
|||
|
|||
```csharp |
|||
public class MyDomainService : DomainService |
|||
{ |
|||
protected IRepository<Book, Guid> BookRepository { get; } |
|||
|
|||
public MyDomainService(IRepository<Book, Guid> bookRepository) |
|||
{ |
|||
BookRepository = bookRepository; |
|||
} |
|||
|
|||
public async Task CreateBooksAsync(List<Book> books) |
|||
{ |
|||
// It'll use bulk inserting automatically. |
|||
await BookRepository.InsertManyAsync(books); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> If you use `ExecuteDeleteAsync` or `ExecuteUpdateAsync`, then ABP's soft delete and auditing features can not work. Because these ABP features work with EF Core's change tracking system and these new methods doesn't work with the change tracking system. So, use them carefully. |
|||
@ -0,0 +1,208 @@ |
|||
# Value generation for DDD guarded types with Entity Framework Core 7.0 |
|||
|
|||
In domain-driven design (DDD), *guarded keys* can improve the type safety of key properties. This is achieved by wrapping the key type in another type which is specific to the use of the key. In this article, I will explain the cases why you may need to use guarded types and discuss the advantages and limitations when implementing to an ABP application. |
|||
|
|||
> You can find the source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo). |
|||
|
|||
## The Problem |
|||
|
|||
While developing an applications, there are many cases where we manually assign foreign keys that can be in guid type or integer type, etc. This manual assignment mistakes can cause miss-match of unique identifiers, such as **assigning a product ID to a category**, that can be hard to detect in the future. |
|||
|
|||
Here is a very simplified sample of wrong assignment when trying to update a product category: |
|||
|
|||
````csharp |
|||
public class ProductAppService : MyProductStoreAppService, IProductAppService |
|||
{ |
|||
private readonly IRepository<Product, Guid> _productRepository; |
|||
|
|||
public ProductAppService(IRepository<Product, Guid> productRepository) |
|||
{ |
|||
_productRepository = productRepository; |
|||
} |
|||
|
|||
public async Task UpdateProductCategoryAsync(Guid productId, Guid categoryId) |
|||
{ |
|||
var productToUpdate = await _productRepository.GetAsync(productId); |
|||
productToUpdate.CategoryId = productId; // Wrong assignment that causes error only at run-time |
|||
|
|||
await _productRepository.UpdateAsync(productToUpdate); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
While the sample demonstrates a very simple mistake, it is easier to come across similar mistakes when the business logic gets more complex especially when you are using methods with **multiple foreign key arguments**. The next section offers using guarded types to prevent these kind of problems as a solution to the problem. |
|||
|
|||
## The Solution |
|||
|
|||
Strongly-typed IDs (*guarded keys*) is a DDD approach to address this problem. One of the main problems with .NET users was handling the persisting these objects. With EFCore7, key properties can be guarded with type safety seamlessly. |
|||
|
|||
To use guarded keys, update your aggregate root or entity unique identifier with a complex type to overcome *primitive obsession*: |
|||
|
|||
````csharp |
|||
public readonly struct CategoryId |
|||
{ |
|||
public CategoryId(Guid value) => Value = value; |
|||
public Guid Value { get; } |
|||
} |
|||
|
|||
public readonly struct ProductId |
|||
{ |
|||
public ProductId(Guid value) => Value = value; |
|||
public Guid Value { get; } |
|||
} |
|||
```` |
|||
|
|||
You can now use these keys for your aggregate roots or entities: |
|||
|
|||
```csharp |
|||
public class Product : AggregateRoot<ProductId> |
|||
{ |
|||
public ProductId Id { get; set; } |
|||
public string Name { get; set; } |
|||
public CategoryId CategoryId { get; set; } |
|||
|
|||
private Product() { } |
|||
|
|||
public Product(ProductId id, string name) : base(id) |
|||
{ |
|||
Name = Check.NotNullOrEmpty(name, nameof(name)); |
|||
} |
|||
} |
|||
|
|||
public class Category : AggregateRoot<CategoryId> |
|||
{ |
|||
public CategoryId Id { get; set; } |
|||
public string Name { get; set; } |
|||
public List<Product> Products { get; } = new(); |
|||
|
|||
private Category() { } |
|||
|
|||
public Category(CategoryId id, string name) : base(id) |
|||
{ |
|||
Name = Check.NotNullOrEmpty(name, nameof(name)); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`ProductId` and `CategoryId` guarded key types shown in the sample use `Guid` key values, which means Guid values will be used in the mapped database tables. This is achieved by defining [value converters](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions) for the types. |
|||
|
|||
Override the `ConfigureConventions` method of your DbContext to use the value converters: |
|||
|
|||
````csharp |
|||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) |
|||
{ |
|||
configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>(); |
|||
configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>(); |
|||
} |
|||
|
|||
private class ProductIdConverter : ValueConverter<ProductId, Guid> |
|||
{ |
|||
public ProductIdConverter() |
|||
: base(v => v.Value, v => new(v)) |
|||
{ |
|||
} |
|||
} |
|||
|
|||
private class CategoryIdConverter : ValueConverter<CategoryId, Guid> |
|||
{ |
|||
public CategoryIdConverter() |
|||
: base(v => v.Value, v => new(v)) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> The code here uses `struct` types. This means they have appropriate value-type semantics for use as keys. If `class` types are used instead, then they need to either override equality semantics or also specify a [value comparer](https://learn.microsoft.com/en-us/ef/core/modeling/value-comparers). |
|||
|
|||
Now, you can use generic (or custom) repositories of ABP using the guarded type as the key for the repository: |
|||
|
|||
```csharp |
|||
public class ProductStoreDataSeedContributor : IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Category, CategoryId> _categoryRepository; |
|||
private readonly IRepository<Product, ProductId> _productRepository; |
|||
|
|||
public ProductStoreDataSeedContributor( |
|||
IRepository<Category, CategoryId> categoryRepository, |
|||
IRepository<Product, ProductId> productRepository |
|||
) |
|||
{ |
|||
_categoryRepository = categoryRepository; |
|||
_productRepository = productRepository; |
|||
} |
|||
|
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
You can also use `integer` as guarded type for your key properties and use [Sequence-based key generation for SQL Server](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#sequence-based-key-generation-for-sql-server) for value generation. |
|||
|
|||
## Discussions |
|||
|
|||
In this section, I will discuss the use cases of guarded types and limitations when implementing to an ABP application. |
|||
|
|||
### Do I need to use guarded types? |
|||
|
|||
If you are already following the best practices of [Domain-Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design), you are aware that **updates** and **creations** of an aggregate are done **over** the aggregate root itself as a whole unit. And entity state changes of an aggregate root should be done using the [domain services](https://docs.abp.io/en/abp/latest/Domain-Services). Domain services should already validate the entity. |
|||
|
|||
**Example: Using domain service to update product:** |
|||
|
|||
````csharp |
|||
public class ProductManager : DomainService |
|||
{ |
|||
private readonly IRepository<Product, Guid> _productRepository; |
|||
|
|||
public ProductManager(IRepository<Product, Guid> productRepository) |
|||
{ |
|||
_productRepository = productRepository; |
|||
} |
|||
|
|||
public Task<Product> AssignCategory(Product product, Category category) |
|||
{ |
|||
// ... |
|||
|
|||
product.CategoryId = category.Id; |
|||
|
|||
//.. |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In this sample, domain service validates that both **product** and the **category** entities, passed by the application layer, are valid objects since they are not key properties. However, manual assignment is already in place and more complex the domain logic, higher to miss out mistakes. At the end, it will depend on your tolerance level for developer errors comparing to the time you want to spend time on additional code base for guarded types. |
|||
|
|||
### Limitations |
|||
|
|||
One important limitation is automatic value generation when using `Guid` as guarded type for your key properties. The basic repository can not generate the unique identifier automatically by the time this article is written: |
|||
|
|||
```csharp |
|||
public readonly struct ProductId |
|||
{ |
|||
public ProductId(Guid value) => Value = value; |
|||
public Guid Value { get; } |
|||
} |
|||
``` |
|||
|
|||
you need to generate the unique identifier **manually**: |
|||
|
|||
````csharp |
|||
var newProduct = await _productRepository.InsertAsync( |
|||
new Product(new ProductId(_guidGenerator.Create()), "New product") |
|||
); |
|||
```` |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I tried to explain DDD guarded types for key properties and value generation for these properties using Entity Framework 7.0 and ABP. |
|||
|
|||
Using strongly-typed key properties reduce the chance of unnoticed errors. Admittedly it increases the code complexity by adding new types to your solution with extra coding, especially if you are using classes as keys. Guarded types provide improved safety for your code base at the expense of additional code complexity as in many DDD concepts and patterns. |
|||
|
|||
If you have a large team working on a large scale solution containing complex business logics where key assignments are abundant or if you are using methods with multiple foreign key arguments, I personally suggest using guarded types. |
|||
|
|||
## The Source Code |
|||
|
|||
* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo). |
|||
|
|||
## See Also |
|||
|
|||
* [What's new in EF Core 7.0](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew) |
|||
* [ABP Framework: Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) |
|||
@ -0,0 +1,231 @@ |
|||
# Rate Limiting with ASP.NET Core 7.0 |
|||
|
|||
Rate limiting is a way of controlling the traffic that a web application or API receives. In other words, rate limiting helps you control the amount of traffic each user has access to at any given time. This is extremely useful when you want to manage the load on your server or services, avoid going over your monthly data transfer limit and allow the system to continue to function and meet service level agreements, even when an increase in demand places an extreme load on resources. |
|||
|
|||
In this article, we will look at what rate limiting is, why we need to use it, how the different rate limiting algorithms provided with .NET 7.0 work, and best practices for using rate limiting in your application. |
|||
|
|||
## What is rate limiting? |
|||
|
|||
Whether accidental or intentional, users may exhaust resources in a way that impacts others. When a number of requests are received on to resources for a long time, the server can run out of those resources. These resources can include memory, threads, connections, or anything else that is limited. To avoid this situation, set rate limiters. Rate limiters control the consumption of resources used by an instance of an application, a user, an individual tenant, or an entire service. |
|||
|
|||
## Why do you need to use rate limiting? |
|||
|
|||
A rate limiting system is crucial in any application where you have to control or throttle user requests or traffic. This is especially true in applications running on a cloud hosting platform because the user’s traffic can affect the whole server where the application is hosted. |
|||
|
|||
Why do you need to implement rate limiting? Here are a few reasons: |
|||
|
|||
- To ensure that a system continues to meet service level agreements (SLA). |
|||
- To prevent a single user, tenant, service, or so on from monopolizing the resources provided by an application. |
|||
- To help cost-optimize a system by limiting the maximum resource levels needed to keep it functioning. |
|||
|
|||
## Rate limiter algorithms |
|||
|
|||
The [`RateLimiterOptionsExtensions`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions) class provides the following extension methods for rate limiting: |
|||
|
|||
- **[Fixed window](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#fixed-window-limit)**: Fixed-window limits—such as 3,000 requests per hour or 10 requests per day—are easy to state, but they are subject to spikes at the edges of the window, as available quota resets. Consider, for example, a limit of 3,000 requests per hour, which still allows for a spike of all 3,000 requests to be made in the first minute of the hour, which might overwhelm the service. |
|||
- [**Sliding window**:](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#sliding-window-limit) Sliding windows have the benefits of a fixed window, but the rolling window of time smoothes out bursts. Systems such as Redis facilitate this technique with expiring keys. |
|||
- [**Token bucket**](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#token-bucket-limit): A token bucket maintains a rolling and accumulating budget of usage as a balance of tokens. A token bucket adds tokens at some rate. When a service request is made, the service attempts to withdraw a token (decrementing the token count) to fulfill the request. If there are no tokens in the bucket, the service has reached its limit and responds with backpressure. |
|||
- [**Concurrency**](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#concurrency-limiter): A concurrency limiter is the simplest form of rate limiting. It doesn’t look at time, just at number of concurrent requests. |
|||
|
|||
In order to be a more realistic example, instead of making an example with each rate limiter algorithm, we will implement the following three algorithms in an **ABP-based** application. |
|||
|
|||
1. We will add a `SlidingWindowLimiter` with a partition for all anonymous users. |
|||
2. We will add a `TokenBucketRateLimiter` with a partition for each authenticated user. |
|||
3. We will add a `ConcurrencyLimiter` with a partition for each Tenant. |
|||
|
|||
**Note:** The following sample isn't meant for production code but is an example of how to use the limiters in ABP-based applications. |
|||
|
|||
### Limiter with `OnRejected`, `RetryAfter`, and `GlobalLimiter` |
|||
|
|||
#### Add rate limiter |
|||
|
|||
Let's create the following method in the `MyProjectNameWebModule.cs` class in the `MyProjectName.Web` project. |
|||
|
|||
**Note:** If the `**.Web` project is not in your application, you can do the same in the project where your application is hosted. |
|||
|
|||
```csharp |
|||
private void ConfigureRateLimiters(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddRateLimiter(limiterOptions => |
|||
{ |
|||
limiterOptions.OnRejected = (context, cancellationToken) => |
|||
{ |
|||
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) |
|||
{ |
|||
context.HttpContext.Response.Headers.RetryAfter = |
|||
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); |
|||
} |
|||
|
|||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; |
|||
context.HttpContext.RequestServices.GetService<ILoggerFactory>()? |
|||
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") |
|||
.LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path); |
|||
|
|||
return new ValueTask(); |
|||
}; |
|||
|
|||
limiterOptions.AddPolicy("UserBasedRateLimiting", context => |
|||
{ |
|||
var currentUser = context.RequestServices.GetService<ICurrentUser>(); |
|||
|
|||
if (currentUser is not null && currentUser.IsAuthenticated) |
|||
{ |
|||
return RateLimitPartition.GetTokenBucketLimiter(currentUser.UserName, _ => new TokenBucketRateLimiterOptions |
|||
{ |
|||
TokenLimit = 10, |
|||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst, |
|||
QueueLimit = 3, |
|||
ReplenishmentPeriod = TimeSpan.FromMinutes(1), |
|||
TokensPerPeriod = 4, |
|||
AutoReplenishment = true |
|||
}); |
|||
} |
|||
|
|||
return RateLimitPartition.GetSlidingWindowLimiter("anonymous-user", |
|||
_ => new SlidingWindowRateLimiterOptions |
|||
{ |
|||
PermitLimit = 2, |
|||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst, |
|||
QueueLimit = 1, |
|||
Window = TimeSpan.FromMinutes(1), |
|||
SegmentsPerWindow = 2 |
|||
}); |
|||
}); |
|||
|
|||
limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => |
|||
{ |
|||
var currentTenant = context.RequestServices.GetService<ICurrentTenant>(); |
|||
|
|||
if (currentTenant is not null && currentTenant.IsAvailable) |
|||
{ |
|||
return RateLimitPartition.GetConcurrencyLimiter(currentTenant!.Name, _ => new ConcurrencyLimiterOptions |
|||
{ |
|||
PermitLimit = 5, |
|||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst, |
|||
QueueLimit = 1 |
|||
}); |
|||
} |
|||
|
|||
return RateLimitPartition.GetNoLimiter("host"); |
|||
}); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
In the above example, the `TokenBucketLimiter` is used for each authenticated user, while the `SlidingWindowLimiter` is used for all anonymous users. Additionally, as a global limiter, the `ConcurrencyLimiter` is used for each tenant, while rate limiting is disabled for the host(tenant is not available). Also, for requests that are rejected when the limit is reached, sets the response status code to [429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) and the response mentions when to retry (if available from the rate-limiting metadata). |
|||
|
|||
Let's call the `ConfigureRateLimiters` method that we created in the `ConfigureServices` method. |
|||
|
|||
The final version of the `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var hostingEnvironment = context.Services.GetHostingEnvironment(); |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
ConfigureBundles(); |
|||
ConfigureUrls(configuration); |
|||
ConfigurePages(configuration); |
|||
ConfigureAuthentication(context); |
|||
ConfigureImpersonation(context, configuration); |
|||
ConfigureAutoMapper(); |
|||
ConfigureVirtualFileSystem(hostingEnvironment); |
|||
ConfigureNavigationServices(); |
|||
ConfigureAutoApiControllers(); |
|||
ConfigureSwaggerServices(context.Services); |
|||
ConfigureExternalProviders(context); |
|||
ConfigureHealthChecks(context); |
|||
ConfigureCookieConsent(context); |
|||
ConfigureTheme(); |
|||
|
|||
Configure<PermissionManagementOptions>(options => |
|||
{ |
|||
options.IsDynamicPermissionStoreEnabled = true; |
|||
}); |
|||
|
|||
ConfigureRateLimiters(context); // added |
|||
} |
|||
``` |
|||
|
|||
#### Add RateLimiter middleware |
|||
|
|||
Add the following line just before the `app.UseConfiguredEndpoints(...)` line to add the `RateLimiter` middleware to your ASP.NET Core request pipeline: |
|||
|
|||
```csharp |
|||
app.UseRateLimiter(); |
|||
``` |
|||
|
|||
#### Use rate limiter for all controllers |
|||
|
|||
Let's edit the `ConfiguredEndpoints` middleware as follows: |
|||
|
|||
```csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapRazorPages() |
|||
.DisableRateLimiting(); |
|||
|
|||
endpoints.MapControllers() |
|||
.RequireRateLimiting("UserBasedRateLimiting"); |
|||
}); |
|||
``` |
|||
|
|||
- **DisableRateLimiting:** It is used to disable the `ConcurrencyLimiter` for razor pages, which we set globally when the tenant is available. |
|||
- **RequireRateLimiting:** We have enabled the rate limiter, which we define according to whether the user is authenticated or not, for all controllers. |
|||
|
|||
## `EnableRateLimiting` and `DisableRateLimiting` attributes |
|||
|
|||
It's kind of unrealistic to always use rate limiting for all controllers or pages. Sometimes, we may want to throttle a particular endpoint or page. In such cases, we can use the `EnableRateLimiting` and `DisableRateLimiting` attributes. The `EnableRateLimiting` and `DisableRateLimiting` attributes can be applied to a controller, action method, or razor rage. Check [here](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#enableratelimiting-and-disableratelimiting-attributes) for more. |
|||
|
|||
## Rate limit an HTTP handler |
|||
|
|||
Rate limiting when sending an HTTP request can be a good practice, especially in service-to-service communication. Because, resources are consumed by apps that rely on them, and when an app makes too many requests for a single resource, it can lead to *resource contention*. Resource contention occurs when a resource is consumed by too many clients, and the resource is unable to serve all of the apps that are requesting it. This can result in a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. Since there are similar codes, I will not mention an example in this article, but to avoid such situations, you can write your own HTTP handler as [here](https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter#implement-a-delegatinghandler-subclass). |
|||
|
|||
## How does it work? |
|||
|
|||
[System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting) provides the primitives for writing rate limiters as well as providing a few commonly used algorithms built-in. The main type is the abstract base class [RateLimiter](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs). |
|||
|
|||
```csharp |
|||
public abstract class RateLimiter : IAsyncDisposable, IDisposable |
|||
{ |
|||
public abstract int GetAvailablePermits(); |
|||
public abstract TimeSpan? IdleDuration { get; } |
|||
|
|||
public RateLimitLease Acquire(int permitCount = 1); |
|||
public ValueTask<RateLimitLease> WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default); |
|||
|
|||
public void Dispose(); |
|||
public ValueTask DisposeAsync(); |
|||
} |
|||
``` |
|||
|
|||
`RateLimiter` contains `Acquire` and `WaitAsync` as the core methods for trying to gain permits for a resource that is being protected. Depending on the application, the protected resource may need to acquire more than 1 permits, so `Acquire` and `WaitAsync` both accept an optional `permitCount` parameter. `Acquire` is a synchronous method that will check if enough permits are available or not and return a `RateLimitLease` which contains information about whether you successfully acquired the permits or not. `WaitAsync` is similar to `Acquire` except that it can support queuing permit requests which can be de-queued at some point in the future when the permits become available, which is why it’s asynchronous and accepts an optional `CancellationToken` to allow canceling the queued request. |
|||
|
|||
`RateLimitLease` has an `IsAcquired` property which is used to see if the permits were acquired. Additionally, the `RateLimitLease` may contain metadata such as a suggested retry-after period if the lease failed. Finally, the `RateLimitLease` is disposable and should be disposed when the code is done using the protected resource. The disposal will let the `RateLimiter` know to update its limits based on how many permits were acquired. |
|||
|
|||
## Limitations |
|||
|
|||
In most cases, the rate-limiting middleware provided with ASP.NET 7.0 will meet your requirements. However, if you would want to return statistics about your limits (e.g. [the way GitHub does](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-http-headers)), you’ll find out that the ASP.NET rate limiting middleware does not support this. You won’t have access to the “number of requests remaining” or other metadata. Not in `OnRejected`, and definitely not if you want to return this data as headers on every request. |
|||
|
|||
## Best practices for rate limiting |
|||
|
|||
In order to use rate limiting properly, you need to have a solid understanding of the types of limiting available, as well as the data rate and data volume of your service. You also need to have a clear idea of how many users you expect to use your service as well as how they will interact with it. The best practices for rate limiting are as follows: |
|||
- Find the right rate limiter algorithm for your endpoint. I mean, the cost of an endpoint should be considered when selecting a limiter. The cost of an endpoint includes the resources used, for example, time, data access, CPU, and I/O. |
|||
- Set realistic limits. Once you’ve figured out all the above, you need to set realistic limits for each service. Then, before deploying an app using rate limiting to production, stress test the app to validate the rate limiters and options used. For example, create a [JMeter script](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) with a tool like [BlazeMeter](https://guide.blazemeter.com/hc/articles/207421695-Writing-your-first-JMeter-script) or [Apache JMeter HTTP(S) Test Script Recorder](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) and load the script to [Azure Load Testing](https://learn.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing). |
|||
- In response to rate-limiting, intermittent, or non-specific errors, a client should generally retry the request after a delay. It is a best practice for this delay to increase exponentially after each failed request, which is referred to as *exponential backoff*. When many clients might be making schedule-based requests (such as fetching results every hour), additional random time (*jitter*) should be applied to the request timing, the backoff period, or both of them to ensure that these multiple client instances don't become periodic [thundering herd](https://www.wikiwand.com/en/Thundering_herd_problem), and cause a form of DDoS themselves. |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, we’ve covered what rate limiting is, why you need to use it and the best practices for doing so. We’ve also looked at how to use three rate-limiting algorithms that are provided with .NET 7.0 on ABP-based applications and how rate-limiting works. Now that you’re familiar with the concept of rate limiting, it’s time to start implementing rate limiting in your application. This will allow you to control the traffic and ensure that your application is running smoothly without any issues. |
|||
|
|||
## References |
|||
|
|||
- https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0 |
|||
- https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/ |
|||
- https://blog.maartenballiauw.be/post/2022/09/26/aspnet-core-rate-limiting-middleware.html |
|||
- https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter |
|||
- https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern |
|||
- https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling |
|||
- https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet |
|||
- https://cloud.google.com/architecture/rate-limiting-strategies-techniques |
|||
|
After Width: | Height: | Size: 625 KiB |
@ -0,0 +1,68 @@ |
|||
# How to Use the Weixin Authentication for MVC / Razor Page Applications |
|||
|
|||
This guide demonstrates how to integrate Weixin to an ABP application that enables users to sign in using OAuth 2.0 with credentials. |
|||
|
|||
## Create a sandbox account |
|||
|
|||
If you don't have a production account, you can create a sendbox account for testing: https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index |
|||
|
|||
In this article we will use the sandbox account. |
|||
|
|||
> You should configure the callback domain name on the Weixin open platform |
|||
|
|||
## AddWeixin |
|||
|
|||
You need to install `AspNet.Security.OAuth.Weixin` package to your **.Web** project. |
|||
|
|||
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
|||
|
|||
```csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); |
|||
context.Services.AddAuthentication() |
|||
.AddWeixin(options => |
|||
{ |
|||
options.ClientId = configuration["Weixin:ClientId"]; |
|||
options.ClientSecret = configuration["Weixin:ClientSecret"]; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Updating `appsettings.json` to add `Weixin` section: |
|||
|
|||
````json |
|||
"Weixin": { |
|||
"ClientId": "<your-app-id>", |
|||
"ClientSecret": "<your-app-secret>" |
|||
} |
|||
```` |
|||
|
|||
## Web page authorization |
|||
|
|||
Now you can run the application to login with Weixin. |
|||
|
|||
 |
|||
|
|||
It will redirect to weixin platform to scan the QR code. |
|||
|
|||
> The sandbox account lacks the necessary scope, so it may not work properly. |
|||
|
|||
## Official account authorization |
|||
|
|||
Updating `AddWeixin`: |
|||
|
|||
```csharp |
|||
context.Services.AddAuthentication() |
|||
.AddWeixin(options => |
|||
{ |
|||
options.ClientId = configuration["Weixin:ClientId"]; |
|||
options.ClientSecret = configuration["Weixin:ClientSecret"]; |
|||
options.AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"; |
|||
}); |
|||
``` |
|||
|
|||
Now you can use WeChat app to open the web application URL to login with weixin. |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,300 @@ |
|||
# What's new with .NET 7? |
|||
|
|||
In this article, I will highlight a few interesting features that are now available with the release of C# 11 and .NET 7. |
|||
|
|||
> If you are considering using ABP 7.0, you should update your projects to .NET 7. There is a good community article you might want to check out 👉 "[Upgrade Your Existing Projects to .NET7](https://community.abp.io/posts/upgrade-your-existing-projects-to-.net7-nmx6vm9m)". |
|||
|
|||
There are many new features coming with this release. We are going to examine new features within 4 sub-sections: ASP.NET Core, C#11, EF 7 and MAUI. Let's start with ASP.NET Core. |
|||
|
|||
## ASP.NET Core |
|||
|
|||
We will see the following features in this section: |
|||
|
|||
* Rate Limiting |
|||
* Output Caching |
|||
* Built-in HTTP/3 Support |
|||
* gRPC - JSON Transcoding |
|||
* Blazor |
|||
* Custom Elements |
|||
* Improvements on JavaScript Interop for WASM |
|||
|
|||
### Rate Limiting |
|||
|
|||
Rate limiting is a way to limit request frequency for a limited time. Before .NET 7 we didn’t have built-in support so we would need to implement it ourselves, use some NuGet packages around or let the CDN provider do this on server level on behalf of us (like Cloudflare). |
|||
|
|||
With .NET 7, built-in Rate Limiting support has been added and we can easily define rate-limiting policies and attach them to our endpoints. |
|||
|
|||
*Defining rate-limiting policy and registering the required services to the DI container*: |
|||
|
|||
 |
|||
|
|||
*Adding the middleware to the request pipeline and using the defined policy*: |
|||
|
|||
 |
|||
|
|||
### Output Caching |
|||
|
|||
Output Caching is a new middleware that provides a caching mechanism and allows you to store results from your web application and serve them from a cache rather than computing them every time. This improves performance and frees up resources for other tasks. |
|||
|
|||
It’s pretty straightforward to use output caching in minimal APIs. |
|||
You just need to create an endpoint and use the `CacheOutput` method with an expiration time. Then when someone sends a request to your endpoint, it will be cached for a specified time and not calculate the result every time. |
|||
|
|||
For the following example, the result will be the same for 10 minutes: |
|||
|
|||
```csharp |
|||
app.MapGet("/cached-output", () => $"Minute: {DateTime.Now.Minute}") |
|||
.CacheOutput(options => |
|||
{ |
|||
output.Expire(TimeSpan.FromMinutes(10)); |
|||
}); |
|||
``` |
|||
|
|||
### Built-in HTTP/3 Support |
|||
|
|||
In .NET 6, HTTP/3 was introduced for experimental purposes and now with .NET 7 it’s fully supported. But it's not enabled by default, it's understandable since it's still new and only %20 of the applications currently use it, on the other hand, HTTP/2 is used by almost every application. |
|||
|
|||
To enable the HTTP/3 support, we need to configure it in our **Program.cs** file: |
|||
|
|||
```csharp |
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
builder.WebHost.ConfigureKestrel((context, options) => |
|||
{ |
|||
options.ListenAnyIP(5001, listenOptions => |
|||
{ |
|||
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; |
|||
listenOptions.UseHttps(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
### gRPC - JSON Transcoding |
|||
|
|||
gRPC is a high-performance RPC framework and uses HTTP/2 and Protobuf. Despite the benefits that gRPC brings, REST APIs have an important place in modern web applications. |
|||
|
|||
gRPC - JSON Transcoding is an extension for ASP.NET Core that creates RESTful JSON APIs for gRPC services. |
|||
|
|||
 |
|||
|
|||
It's a really good feature and allows us to expose our gRPC services as REST APIs. It's worth checking out 👉 [JSON Transcoding](https://learn.microsoft.com/en-us/aspnet/core/grpc/json-transcoding?view=aspnetcore-7.0). |
|||
|
|||
> I've created a separate community article for this feature. Check it from [here](https://community.abp.io/posts/grpc-json-transcoding-16eai2zw). |
|||
|
|||
### Blazor |
|||
|
|||
We are going to cover two new features for Blazor: |
|||
|
|||
* Custom Elements |
|||
* Improvements on JavaScript Interop for WASM |
|||
|
|||
#### Custom Elements |
|||
|
|||
Blazor Custom Elements provide a way to dynamically render Razor Components from other SPA frameworks/libraries such as Angular and React. |
|||
|
|||
To be able to use custom elements, there are two steps that need to be done: |
|||
|
|||
**1-) Registering a Razor Component as a Custom Element:** |
|||
|
|||
```csharp |
|||
builder.Services.AddServerSideBlazor(options => |
|||
{ |
|||
options.RootComponents.RegisterCustomElement<Counter>("my-counter"); |
|||
}); |
|||
``` |
|||
|
|||
**2-) Using the Custom Element** |
|||
|
|||
```html |
|||
<my-counter increment-amount={incrementAmount} /> |
|||
``` |
|||
|
|||
#### Improvements on JavaScript Interop for WASM |
|||
|
|||
JavaScript `[JsImport]` / `[JsExport]` interop API released with .NET 7. |
|||
|
|||
* To import a JS function to call it from C# -> `[JsImport]` |
|||
* To export a .NET method so that it can be called from JavaScript -> `[JsExport]` attributes should be used. |
|||
|
|||
## C# 11 |
|||
|
|||
There are great features that came with C# 11. In this article, we are going to only cover the following ones: |
|||
|
|||
* Required Members |
|||
* Generic Attributes |
|||
* Raw String Literals |
|||
* List Patterns |
|||
|
|||
### Required Members |
|||
|
|||
C# 11 introduces a new **required** keyword to allow us to ensure property initialization on object creation. |
|||
|
|||
We just need to use the **required** keyword before the property type. That’s it. Then if we try to create an object without initializing the required properties, compile-time errors will be shown: |
|||
|
|||
 |
|||
|
|||
### Generic Attributes |
|||
|
|||
Generic Attributes are also one of the good features that came with C# 11. Before C#11, creating a typed attribute was cumbersome. We would need to pass a **Type** object as an argument to our constructor and assign it to a property in our attribute class and do stuff with this **Type** property. |
|||
|
|||
With C#11, Generic Attributes are introduced. Now, it’s possible to easily create generic attributes like creating generic classes: |
|||
|
|||
```csharp |
|||
//defining a generic attribute |
|||
public class GenericAttribute<T> : Attribute { } |
|||
|
|||
//using the attribute |
|||
[GenericAttribute<int>] |
|||
public int MyMethod() => default; |
|||
``` |
|||
|
|||
### Raw String Literals |
|||
|
|||
Raw String Literals is a great syntactic sugar that came with C#11. It allows containing of arbitrary text without escaping. |
|||
|
|||
By wrapping a string with three double quotes (”””...”””), we are free to put any string value into variables: |
|||
|
|||
```csharp |
|||
var jsonContent = """ |
|||
{ |
|||
"name": ".NET 7", |
|||
"feature": "Raw String Literals" |
|||
} |
|||
"""; |
|||
``` |
|||
|
|||
Also, we can use string interpolation with this new syntax. We just need to add a $ sign before the first triple quotes: |
|||
|
|||
 |
|||
|
|||
### List Patterns |
|||
|
|||
C# 11 introduces the “List Pattern”. It expands the pattern matching for lists and arrays. There are three different ways for list pattern matching: |
|||
|
|||
#### 1-) Discard Pattern |
|||
|
|||
This pattern can be helpful to match one or more elements from a sequence if we know the length of the sequence. |
|||
|
|||
 |
|||
|
|||
#### 2-) Range Pattern |
|||
|
|||
If the length of the sequence is not known, then the range pattern may be useful. We can use this pattern to check the first or/and last element from a sequence. |
|||
|
|||
 |
|||
|
|||
#### 3-) var Pattern |
|||
|
|||
This pattern allows us to capture an element at that position and use the variable in our code. |
|||
|
|||
 |
|||
|
|||
## Entity Framework Core 7 |
|||
|
|||
There are too many improvements and new features that were shipped with EF Core 7. Here's what we're going to cover in this article: |
|||
|
|||
* JSON Columns |
|||
* Improvements on Bulk Updates & Deletes |
|||
* Performance Improvements on SaveChanges & SaveChangesAsync |
|||
|
|||
### JSON Columns |
|||
|
|||
EF 7 supports JSON Columns and this allows the mapping of aggregates built from .NET types to JSON documents. |
|||
|
|||
Thus, it's kind of combines relational databases and document-based databases in a way. |
|||
|
|||
We can easily mark a column as a JSON column on the `OnModelCreating` method of our `DbContext` class: |
|||
|
|||
```csharp |
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
modelBuilder.Entity<Author>().OwnsOne( |
|||
author => author.Contact, ownedNavigationBuilder => |
|||
{ |
|||
ownedNavigationBuilder.ToJson(); |
|||
|
|||
ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Also with this version, LINQ JSON query support has been announced. So, we can query over JSON Columns using LINQ as below: |
|||
|
|||
```csharp |
|||
var posts = await context.Posts |
|||
.AsNoTracking() |
|||
.Select( |
|||
post => new |
|||
{ |
|||
post.Author!.Name, |
|||
post.Metadata!.Views, |
|||
Searches = post.Metadata.TopSearches, |
|||
Commits = post.Metadata.Updates |
|||
}) |
|||
.ToListAsync(); |
|||
``` |
|||
|
|||
### Improvements on Bulk Updates & Deletes |
|||
|
|||
EF 7 introduces the new **ExecuteUpdateAsync** and **ExecuteDeleteAsync** methods. By using these methods while making bulk updates or deletes, we can take out the change tracker in this process and this brings great performance gains. |
|||
|
|||
Example: |
|||
|
|||
```csharp |
|||
await (await GetDbContextAsync()).Tags |
|||
.Where(tag => tag.Description.Contains("ABP")) |
|||
.ExecuteDeleteAsync(); |
|||
``` |
|||
|
|||
### Performance Improvements on SaveChanges & SaveChangesAsync |
|||
|
|||
With EF 7, there are significant performance improvements on SaveChanges & SaveChangesAsync methods. According to the EF Core team, in some cases, saving changes is now four times faster than EF 6. You can see a simple benchmark result from the EF Core blog post here. |
|||
|
|||
 |
|||
|
|||
Even after inserting just four records, there is a significant performance gain. |
|||
|
|||
## .NET MAUI 7 |
|||
|
|||
As you know, .NET MAUI is a cross-platform framework for creating native mobile and desktop applications by using C# and XAML. By using the .NET MAUI, apps that can run on Android, IOS, macOS and Windows from a single-code base can be developed. |
|||
|
|||
It’s a new technology, so it's evolving and the .NET MAUI team introduces good features with every release. |
|||
|
|||
In this article, I will only mention a new feature called "Map Control" and some enhancements made with this release. |
|||
|
|||
### Map Control |
|||
|
|||
.NET MAUI 7 introduces **Map Control**. This provides us a good native map experience. |
|||
|
|||
It supports pins, polygons, circles, geolocations and much more... |
|||
|
|||
 |
|||
|
|||
### Improvements on Mobile Rendering & Desktop Enhancements |
|||
|
|||
.NET MAUI 7 came with optimized rendering for mobile applications and is much faster than .NET MAUI 6. |
|||
|
|||
Also, there are some good enhancements on the desktop side: |
|||
|
|||
* Window size and position, |
|||
* Context Menus (which there was a kind of bug and was not seen on some MAUI Controls for Windows applications), |
|||
* Tooltips, |
|||
* Gestures and more... |
|||
|
|||
--- |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I've highlighted some features that were shipped with .NET 7. |
|||
|
|||
> I've added a references section down below, so you can check the references and see other features that came with this version. |
|||
|
|||
Thanks for reading the article and I hope you find it helpful. See you in the next one! |
|||
|
|||
## References |
|||
|
|||
* https://devblogs.microsoft.com/dotnet/announcing-dotnet-7/ |
|||
* https://devblogs.microsoft.com/dotnet/announcing-asp-net-core-in-dotnet-7 |
|||
* https://devblogs.microsoft.com/dotnet/announcing-ef7 |
|||
* https://devblogs.microsoft.com/dotnet/dotnet-maui-dotnet-7 |
|||
* https://devblogs.microsoft.com/dotnet/welcome-to-csharp-11 |
|||
* https://www.milanjovanovic.tech/blog/whats-new-in-dotnet-7 |
|||
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 534 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 52 KiB |
@ -0,0 +1,86 @@ |
|||
# Converting Create/Edit Modal to Page |
|||
|
|||
In this document we will explain how to convert BookStore's `Books` create & edit modals to regular razor pages. |
|||
|
|||
## Before |
|||
 |
|||
|
|||
## Now |
|||
 |
|||
|
|||
## Index page |
|||
|
|||
Repalce `abp-button(NewBookButton)` buttom with `<a class="btn btn-primary" href="/Books/CreateModal"><i class="fa fa-plus"></i> @L["NewBook"].Value</a>`. |
|||
|
|||
## Index js file |
|||
|
|||
Remove the related codes of `createModal` and `editModal`. |
|||
|
|||
Change the `Edit row action` with `location.href = "/Books/EditModal?id=" + data.record.id;` |
|||
|
|||
|
|||
## Create/Edit Book page |
|||
|
|||
Remove `Layout = null;` and add some custom style and javascript code to `CreateModal.cshtml` & `EditModal.cshtml`. |
|||
|
|||
```csharp |
|||
@section styles { |
|||
<style> |
|||
.abp-view-modal .modal { |
|||
position: static; |
|||
display: block; |
|||
opacity: inherit !important; |
|||
} |
|||
.abp-view-modal .modal.fade .modal-dialog { |
|||
transition: inherit !important; |
|||
transform: inherit !important;; |
|||
} |
|||
.abp-view-modal .modal-header .btn-close { |
|||
display: none; |
|||
} |
|||
</style> |
|||
} |
|||
@section scripts { |
|||
<script> |
|||
$(".abp-view-modal form").abpAjaxForm().on('abp-ajax-success', function () { |
|||
location.href = "/Books"; |
|||
}); |
|||
</script> |
|||
} |
|||
``` |
|||
|
|||
Add a `div` element with `abp-view-modal` class to wrap the `abp-dynamic-form`, Set size of `abp-modal` to `ExtraLarge` and remove the `AbpModalButtons.Cancel` button from `abp-modal-footer`. |
|||
|
|||
### CreateModal |
|||
```csharp |
|||
<div class="abp-view-modal"> |
|||
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal"> |
|||
<abp-modal static="true" size="ExtraLarge"> |
|||
<abp-modal-header title="@L["NewBook"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
</div> |
|||
``` |
|||
|
|||
### EditModal |
|||
```csharp |
|||
<div class="abp-view-modal"> |
|||
<abp-dynamic-form abp-model="Book" asp-page="/Books/EditModal"> |
|||
<abp-modal size="ExtraLarge"> |
|||
<abp-modal-header title="@L["Update"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
</div> |
|||
``` |
|||
|
|||
You can check this Git commit for details. |
|||
|
|||
https://github.com/abpframework/abp-samples/commit/f3014e0ec422cb2d8816d0e00dd6ab9cc1adfc21 |
|||
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
@ -0,0 +1,19 @@ |
|||
# ABP Version 7.1 Migration Guide |
|||
|
|||
This document is a guide for upgrading ABP v7.0 solutions to ABP v7.1. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. |
|||
|
|||
## Navigation Menu - `CustomData` type changed to `Dictionary<string, object>` |
|||
|
|||
`ApplicationMenu` and `ApplicationMenuItem` classes' `CustomData` property type has been changed to `Dictionary<string, object>`. So, if you use the optional `CustomData` property of these classes, change it accordingly. See [#15608](https://github.com/abpframework/abp/pull/15608) for more information. |
|||
|
|||
*Old usage:* |
|||
|
|||
```csharp |
|||
var menu = new ApplicationMenu("Home", L["Home"], "/", customData: new MyCustomData()); |
|||
``` |
|||
|
|||
*New usage:* |
|||
|
|||
```csharp |
|||
var menu = new ApplicationMenu("Home", L["Home"], "/").WithCustomData("CustomDataKey", new MyCustomData()); |
|||
``` |
|||