mirror of https://github.com/abpframework/abp.git
committed by
GitHub
286 changed files with 3640 additions and 2028 deletions
@ -0,0 +1,108 @@ |
|||
# Optimizing Static Asset Delivery feature in ASP.NET Core 9.0 |
|||
|
|||
Delivering static assets efficiently is a key factor in building performant web applications. By optimizing how assets like CSS, JavaScript, and images are served to the browser, you can reduce load times, decrease network traffic, and improve the overall user experience. |
|||
|
|||
One powerful tool to help achieve this is **MapStaticAssets**, a feature in ASP.NET Core that significantly optimizes the delivery of static resources. Whether you're working with Blazor, Razor Pages, MVC, or other UI frameworks, **MapStaticAssets** streamlines asset management and ensures that your web app delivers resources in the most efficient way possible. |
|||
|
|||
## Why Optimizing Static Assets Matters |
|||
|
|||
Serving static assets without optimization can lead to several performance bottlenecks: |
|||
|
|||
- **Excessive network requests**: The browser may need to request the same resources multiple times, even if they haven’t changed. |
|||
- **Unnecessary data transfer**: Larger files are sent over the network, consuming bandwidth and slowing down page loads. |
|||
- **Outdated assets**: Without proper cache management, users may receive stale versions of files after an app update. |
|||
|
|||
Optimizing static assets involves compressing files, managing caching headers, and ensuring that only the necessary resources are sent to the client. **MapStaticAssets** takes care of all these issues in a seamless, automated way. |
|||
|
|||
## What is MapStaticAssets? |
|||
|
|||
**MapStaticAssets** is designed to enhance the default static asset serving mechanism in ASP.NET Core. It can replace `UseStaticFiles` in most scenarios and comes with several built-in optimizations. These optimizations are executed at both build and publish time, ensuring that static resources are served in the most efficient way possible when your app is running. |
|||
|
|||
Here's how you can implement **MapStaticAssets** in your app: |
|||
|
|||
```csharp |
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
builder.Services.AddRazorPages(); |
|||
|
|||
var app = builder.Build(); |
|||
|
|||
if (!app.Environment.IsDevelopment()) |
|||
{ |
|||
app.UseExceptionHandler("/Error"); |
|||
app.UseHsts(); |
|||
} |
|||
|
|||
app.UseHttpsRedirection(); |
|||
|
|||
app.UseRouting(); |
|||
|
|||
app.UseAuthorization(); |
|||
|
|||
// Replacing UseStaticFiles with MapStaticAssets |
|||
app.MapStaticAssets(); |
|||
app.MapRazorPages(); |
|||
|
|||
app.Run(); |
|||
``` |
|||
|
|||
## Key Features of MapStaticAssets |
|||
|
|||
1. **Build-time Compression**: |
|||
**MapStaticAssets** automatically compresses all static assets during the build process. It uses **gzip** compression during development and **gzip + brotli** compression when publishing. This reduces the file size significantly, ensuring faster download times. |
|||
|
|||
For example, in a default Razor Pages template, assets like `bootstrap.min.css` and `jquery.js` are compressed by over 80%, resulting in significantly reduced file sizes: |
|||
|
|||
| File | Original Size | Compressed Size | Compression Reduction | |
|||
|----------------------|---------------|-----------------|-----------------------| |
|||
| `bootstrap.min.css` | 163 KB | 17.5 KB | 89.26% | |
|||
| `jquery.js` | 89.6 KB | 28 KB | 68.75% | |
|||
| `bootstrap.min.js` | 78.5 KB | 20 KB | 74.52% | |
|||
| **Total** | 331.1 KB | 65.5 KB | 80.20% | |
|||
|
|||
2. **Content-based ETags**: |
|||
**MapStaticAssets** generates **ETags** based on the SHA-256 hash of the file content, encoded in Base64. This ensures that the browser only re-downloads a resource if its content has changed. This eliminates unnecessary network requests, improving page load speeds. |
|||
|
|||
3. **Smaller File Sizes for Libraries**: |
|||
Popular component libraries, such as **Fluent UI Blazor** and **MudBlazor**, benefit from similar compression optimizations. For example, the size of the **MudBlazor** library is reduced by over 90%, from 588 KB to just 46.7 KB after compression. |
|||
|
|||
| File | Original Size | Compressed Size | Compression Reduction | |
|||
|----------------------|---------------|-----------------|-----------------------| |
|||
| `MudBlazor.min.css` | 541 KB | 37.5 KB | 93.07% | |
|||
| `MudBlazor.min.js` | 47.4 KB | 9.2 KB | 80.59% | |
|||
| **Total** | 588.4 KB | 46.7 KB | 92.07% | |
|||
|
|||
4. **Automatic Optimization**: |
|||
As libraries or components are added or updated, **MapStaticAssets** automatically optimizes the assets as part of the build process. This includes minimizing the size of JavaScript and CSS files, reducing the impact of mobile or low-bandwidth environments. |
|||
|
|||
5. **Serving Assets with a CDN**: |
|||
Although **MapStaticAssets** is focused on server-side optimizations, integrating a **CDN (Content Delivery Network)** can further boost performance by serving static assets from servers geographically closer to the user, reducing latency. |
|||
|
|||
## Comparing MapStaticAssets to IIS Dynamic Compression |
|||
|
|||
**MapStaticAssets** provides several advantages over traditional dynamic compression techniques, such as IIS **gzip** compression: |
|||
|
|||
- **Simplicity**: There is no need for server-specific configuration, making **MapStaticAssets** easy to implement. |
|||
- **Performance**: By compressing assets at build time, the app doesn't need to perform compression during every request, which improves server performance. |
|||
- **Optimization**: Developers can focus on ensuring that assets are compressed to the smallest possible size during the build process. |
|||
|
|||
For example, using **MapStaticAssets**, a file like `MudBlazor.min.css` is compressed down to 37.5 KB, whereas IIS dynamic compression might result in a size of 90 KB. This represents a **59%** reduction in size. |
|||
|
|||
## About MapAbpStaticAssets |
|||
|
|||
The ABP framework is 100% compatible with this new feature. |
|||
|
|||
However, some JavaScript, CSS, and image files exist in the [Virtual File System](https://abp.io/docs/latest/framework/infrastructure/virtual-file-system), which ASP.NET Core's **MapStaticAssets** can't handle. For these files, additional **StaticFileMiddleware** is needed to serve them, which is where **MapAbpStaticAssets** comes in. |
|||
|
|||
**MapAbpStaticAssets** adds the necessary **StaticFileMiddleware** to ensure that virtual files are correctly served. This middleware setup ensures seamless delivery of virtual resources alongside static assets. |
|||
|
|||
You can view the source code of **MapAbpStaticAssets** on [GitHub](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs#L129-L198). |
|||
|
|||
## Conclusion |
|||
|
|||
Optimizing static asset delivery is essential for building fast, efficient web applications. **MapStaticAssets** simplifies and automates the optimization of static files by providing build-time compression, caching headers, and content-based ETags. This ensures that your app's static assets are always delivered in the most efficient way, whether users are on fast broadband or slower mobile connections. By using **MapStaticAssets**, you can deliver a faster, more reliable experience for your users with minimal effort. |
|||
|
|||
## References |
|||
|
|||
* [Static files in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-9.0) |
|||
* [What's new in ASP.NET Core 9.0](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#optimize-static-web-asset-delivery) |
|||
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 106 KiB |
@ -0,0 +1,164 @@ |
|||
Built-in OpenAPI Document Generation with .NET 9 — No more SwaggerUI! 👋 |
|||
======================================================================== |
|||
|
|||
 |
|||
|
|||
What’s Swagger UI? |
|||
------------------ |
|||
|
|||
[Swagger UI](https://swagger.io/) is an open-source tool that automatically generates an interactive, web-based documentation interface for WebAPIs. |
|||
It supports OpenAPI standards. It was very popular tool among the ASP.NET Core developers from 2020 to 2024. |
|||
Because it was a built-in tool comes with ASP.NET Core default templates. |
|||
We liked this tool because it was the first tool that allows us to make WebAPI calls for testing. |
|||
Now it provides paid services as well as free ones. |
|||
|
|||
> Previously, Swagger was included by default from **.NET 5** to **.NET 8** in .NET web templates. |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
What’s OpenAPI? |
|||
--------------- |
|||
|
|||
OpenAPI is a standard specification for defining REST APIs. |
|||
The official website is [https://www.openapis.org/](https://www.openapis.org/). |
|||
Microsoft is now using OpenAPI and here is the official documentation 👉 [https://aka.ms/aspnet/openapi](https://aka.ms/aspnet/openapi) |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
Replacement of Swagger UI with OpenAPI |
|||
---------------------------------------------------------------------------- |
|||
|
|||
Swagger UI is no longer integrated into NET 9, as Microsoft wants a solution with first-class support, better control, and enhanced security. As you see in the below screenshot, Microsoft declares that it's already removed. |
|||
|
|||
 |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
## Why is Swagger Removed from .NET 9? |
|||
|
|||
In March 2024, the ASP.NET Core team announced that they are removing the `Swashbuckle.AspNetCore` dependency from web templates from .NET 9 release. |
|||
|
|||
|
|||
|
|||
> This decision was influenced by the project's lack of active maintenance and the absence of an official release for .NET 8. |
|||
|
|||
|
|||
|
|||
Microsoft team created a new package `Microsoft.AspNetCore.OpenApi`. It provides built-in OpenAPI document generation just like Swagger. So Microsoft doesn't depend on external tools. Because in every .NET release, they need to ask the owners of the external tool libraries to align with their new version. And sometimes these library owners cannot update their code-base according to the recent .NET changes. And it is becoming harder for Microsoft to support the 3rd party libraries under these circumstances. Basically reducing 3rd party dependencies will help Microsoft fast release cycles. |
|||
|
|||
I read Reddit, GitHub discussions and YouTube reviews about this topic. As I see community members expressed concerns about the inactivity of Swashbuckle and they are discussing alternatives like contributing to or forking the project. The Microsoft team also contacted the owners of Swashbuckle and NSwag to explore potential collaborations and ensure a smooth transition for developers. |
|||
|
|||
In the below GitHub issue, you can see the details of this decision: |
|||
|
|||
* [github.com/dotnet/aspnetcore/issues/54599](https://github.com/dotnet/aspnetcore/issues/54599) |
|||
|
|||
|
|||
|
|||
**Jeremy** -Product Manager- at Microsoft, answers why they took this decision in [this post](https://github.com/dotnet/aspnetcore/issues/54599#issuecomment-2004975574). |
|||
|
|||
 |
|||
|
|||
As a summary; |
|||
|
|||
**The change is due to a lack of maintenance of the Swagger library**, although it has seen some recent updates. This aims to reduce dependency on external tools and provide a streamlined, out-of-the-box experience for generating OpenAPI documentation for ASP.NET Core Web APIs. |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
What are the Benefits of the New OpenAI Package? |
|||
--------------------------------------------------------------- |
|||
|
|||
### Native Support and Reduced Dependency |
|||
|
|||
The new `Microsoft.AspNetCore.OpenApi` package provides first-class citizen support for OpenAPI. It reduces reliance on external tools like Swashbuckle or NSwag for basic documentation needs. The native implementation leverages source generators to reduce runtime overhead. |
|||
|
|||
### Simplified Configuration |
|||
|
|||
No need extra setup or 3rd party integrations. Just by defining controllers and endpoints, ASP.NET Core automatically generates OpenAPI specifications. |
|||
|
|||
### Well Integration with Minimal APIs |
|||
|
|||
[Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis) introduced in .NET 6. There's an optimized built-in support for Minimal APIs. It automatically adds metadata for routes, request parameters, and responses. |
|||
|
|||
### Compatibility with Existing Tools |
|||
|
|||
You can still use the output of OpenAPI with Swagger or NSwag... So it doesn't mean that in this case you have only one option when you use OpenAPI. |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
How to Use the New OpenAPI in .NET9? |
|||
------------------------------------ |
|||
|
|||
When you create a new ASP.NET Core project, you can see the below checkbox to add OpenAPI. |
|||
|
|||
 |
|||
|
|||
I created a new .NET 9 web project, I saw that OpenAPI had already been added. |
|||
|
|||
 |
|||
|
|||
## Add OpenAPI Support For Your Existing Project |
|||
|
|||
Upgrade your project to .NET 9 and add the required NuGet package [Microsoft.AspNetCore.OpenApi](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) |
|||
|
|||
``` |
|||
dotnet add package Microsoft.AspNetCore.OpenApi |
|||
``` |
|||
|
|||
### |
|||
|
|||
Add the following services and middleware in `Program.cs` |
|||
|
|||
``` |
|||
var builder = WebApplication.CreateBuilder(); |
|||
builder.Services.AddOpenApi(); //<<----- |
|||
var app = builder.Build(); |
|||
app.MapOpenApi(); //<<----- |
|||
app.MapGet("/", () => "Test"); |
|||
app.Run(); |
|||
``` |
|||
|
|||
Your OpenAPI document URL is [_https://localhost:7077/openapi/v1.json_](https://localhost:7077/openapi/v1.json) |
|||
|
|||
Change the port to your active port. This is how it looks like: |
|||
|
|||
 |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
Alternative 3rd Party Tool: Scalar |
|||
================================== |
|||
|
|||
**Scalar** is an open-source API platform for RestAPI documentation. Also, it provides an interface for interacting with RESTful API. Generates interactive and user-friendly API documentation. Supports OpenAPI and Swagger specifications. It’s open-source with **7K stars** on GitHub. |
|||
|
|||
See the repo 👉 [https://github.com/scalar/scalar](https://github.com/scalar/scalar). |
|||
|
|||
|
|||
|
|||
That's all from the replacement of Swagger in .NET 9. |
|||
Happy coding 👨💻 |
|||
|
|||
|
|||
|
|||
**References** |
|||
|
|||
* [https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#openapi](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#openapi) |
|||
|
|||
|
|||
@ -0,0 +1,202 @@ |
|||
# C# 13 Features |
|||
|
|||
C# 13 is the latest version of C# and it comes with a lot of new features. In this article, we will discuss some of the new features of C# 13. |
|||
|
|||
## `params` collections |
|||
|
|||
With the C# 13, method parameter with `params` keyword isn't limited to be an array. You can now use any collection type that implements `IEnumerable<T>` interface. |
|||
|
|||
Let's see how it can help us in our code. |
|||
|
|||
```csharp |
|||
public IEnumerable<int> GetOdds(params IEnumerable<int> numbers) |
|||
{ |
|||
foreach (var number in numbers) |
|||
{ |
|||
if (number % 2 != 0) |
|||
{ |
|||
Console.WriteLine(number); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## New lock object |
|||
|
|||
I'm sure you have used `lock` statement in your code to synchronize access to a shared resource. With C# 13, you can now use a new lock object that is more efficient than the traditional lock object. |
|||
The new `Lock` type provides better thread synchronization through its API. When `Lock.EnterScope()` method is called, it returns a struct named `Scope` that contains a `Dispose` method. The `Dispose` method is called when the `Scope` object goes out of scope, which releases the lock. C# `using` statement recognizes the `Dispose` method and calls it automatically like it does with other `IDisposable` objects. |
|||
|
|||
It was something similar before: |
|||
```csharp |
|||
private object _lock = new(); |
|||
|
|||
public void DoSomething() |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
// Do something |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Now, you can use the new lock object like this: |
|||
```csharp |
|||
System.Threading.Lock x = new System.Threading.Lock(); |
|||
public void DoSomething() |
|||
{ |
|||
using (x.EnterScope()) |
|||
{ |
|||
// Do something |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## New escape sequence |
|||
In C# 13, a new escape sequence `\e` has been introduced to represent the `ESCAPE` character, Unicode `U+001B`. Previously, you had to use `\u001b` or `\x1b` to represent this character. The new `\e` escape sequence simplifies this process and avoids potential issues with hexadecimal digits following `\x1b`. |
|||
|
|||
> You can check [here](https://en.wikipedia.org/wiki/ANSI_escape_code#C0_control_codes) for ANSI escape codes. |
|||
|
|||
## Implicit index access |
|||
|
|||
The implicit "from the end" index operator, `^`, is now allowed in an object initializer expression. |
|||
|
|||
It was not possible before, but now you can do this: |
|||
|
|||
```csharp |
|||
var countdown = new TimerRemaining() |
|||
{ |
|||
buffer = |
|||
{ |
|||
[^1] = 0, |
|||
[^2] = 1, |
|||
[^3] = 2, |
|||
[^4] = 3, |
|||
[^5] = 4, |
|||
[^6] = 5, |
|||
[^7] = 6, |
|||
[^8] = 7, |
|||
[^9] = 8, |
|||
[^10] = 9 |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
It's a great feature that makes the code more readable and maintainable. Still not a big deal, but it's nice to have it. |
|||
|
|||
## `ref` and `unsafe` in iterators and async methods |
|||
|
|||
In C# 13, the restrictions on using `ref` and `unsafe` constructs in iterators and async methods have been relaxed. Previously, you couldn't declare local `ref` variables or use unsafe contexts in these methods. Now, you can declare ref local variables and use unsafe contexts in async methods and iterators, provided they are not accessed across `await` or `yield` boundaries |
|||
|
|||
|
|||
This change allows for more expressive and efficient code, especially when working with types like `System.Span<T>` and `System.ReadOnlySpan<T>`. The compiler ensures that these constructs are used safely, and it will notify you if any safety rules are violated. |
|||
|
|||
You can read more about this feature on the [Microsoft Learn page](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-13.0/ref-unsafe-in-iterators-async). |
|||
|
|||
|
|||
|
|||
## More partial members |
|||
|
|||
In C# 13, the concept of partial members has been expanded to include partial properties and partial indexers. Previously, only methods could be defined as partial members. This means you can now split the definition of properties and indexers across multiple files, just like you could with methods. |
|||
|
|||
For example, you can declare a partial property in one part of your class and implement it in another part. Here's a simple illustration: |
|||
|
|||
```csharp |
|||
public partial class MyClass |
|||
{ |
|||
// Declaring declaration |
|||
public partial string MyProperty { get; set; } |
|||
} |
|||
|
|||
public partial class MyClass |
|||
{ |
|||
// Implementing declaration |
|||
private string _myProperty; |
|||
public partial string MyProperty |
|||
{ |
|||
get => _myProperty; |
|||
set => _myProperty = value; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
This feature allows for better organization and modularization of your code, especially in large projects where different parts of a class might be implemented by different team members. |
|||
|
|||
## Overload resolution priority |
|||
|
|||
What does "Overload resolution priority" section mean in this page? |
|||
In C# 13, the OverloadResolutionPriority attribute allows library authors to specify which method overload should be preferred by the compiler when multiple overloads are available. This attribute helps avoid ambiguity and ensures that the most appropriate overload is chosen, even if it might not be the most obvious choice based on traditional overload resolution rules. |
|||
|
|||
This may be useful in scenarios where you have multiple overloads that are equally valid, but you want to prioritize one over the others. The attribute can be applied to a method or constructor to indicate its priority in the overload resolution process. It can prevent unexpected behavior and make your code more predictable and maintainable. |
|||
|
|||
|
|||
Let me show with an example: |
|||
```csharp |
|||
public class Example |
|||
{ |
|||
// Existing method |
|||
public void Display(string message = "Hello!") |
|||
{ |
|||
Console.WriteLine("Message: " + message); |
|||
} |
|||
|
|||
// New, more efficient method with higher priority |
|||
[OverloadResolutionPriority(1)] |
|||
public void Display(string message = "Hello!", int repeatCount = 3) |
|||
{ |
|||
for (int i = 0; i < repeatCount; i++) |
|||
{ |
|||
Console.WriteLine("Message: " + message); |
|||
} |
|||
} |
|||
} |
|||
|
|||
class Program |
|||
{ |
|||
static void Main() |
|||
{ |
|||
Example example = new Example(); |
|||
|
|||
// Normally, you can't compile this code because of ambiguity: |
|||
example.Display(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Output: |
|||
``` |
|||
Message: Hello! |
|||
Message: Hello! |
|||
Message: Hello! |
|||
``` |
|||
|
|||
## The `field` keyword |
|||
|
|||
n C# 13, the `field` keyword is introduced as a preview feature to simplify property accessors. This keyword allows you to reference the compiler-generated backing `field` directly within a property accessor, eliminating the need to declare an explicit backing `field` in your type declaration. |
|||
|
|||
For example, instead of writing: |
|||
|
|||
```csharp |
|||
private int _value; |
|||
public int Value |
|||
{ |
|||
get => _value; |
|||
set => _value = value; |
|||
} |
|||
``` |
|||
|
|||
You can now write: |
|||
|
|||
```csharp |
|||
public int Value |
|||
{ |
|||
get => field; |
|||
set => field = value; |
|||
} |
|||
``` |
|||
|
|||
This makes your code cleaner and more concise. However, be cautious if you have a `field` named `field` in your class, as it could cause confusion. You can disambiguate by using `@field` or `this.field`. |
|||
|
|||
Make sure you're using the latest `LangVersion` in your `.csproj` project file to enable this feature. |
|||
```xml |
|||
<LangVersion>preview</LangVersion> |
|||
``` |
|||
@ -0,0 +1,165 @@ |
|||
# EF Core 9 LINQ & SQL translation |
|||
|
|||
EF Core improves the translation of LINQ queries to SQL with every release. EF Core 9 is no exception. This article will show you some of the improvements in EF Core 9. |
|||
|
|||
EF Core 9 includes a lot of improvements in LINQ to SQL translation. we don't cover all of them in this article. You can find more information in the [official release notes](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#linq-and-sql-translation). |
|||
|
|||
## Support for complex types |
|||
|
|||
### GroupBy |
|||
|
|||
EF Core now supports grouping by complex type instance. For example: |
|||
|
|||
```csharp |
|||
var groupedAddress = await context.Customers |
|||
.GroupBy(c => new { c.Address }) |
|||
.Select(g => new { g.Key, Count = g.Count() }) |
|||
.ToListAsync(); |
|||
``` |
|||
|
|||
Address is a complex type as a value object here. |
|||
|
|||
### ExecuteUpdate |
|||
|
|||
EF Core now supports updating a complex type. For example: |
|||
|
|||
```csharp |
|||
var newAddress = new Address("New Street", "New City", "New Country"); |
|||
|
|||
await context.Customers |
|||
.Where(e => e.Region == "Turkey") |
|||
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Address, newAddress)); |
|||
``` |
|||
|
|||
EF Core updates each column of the complex type. |
|||
|
|||
## Prune unneeded elements from SQL |
|||
|
|||
Ef Core now translates LINQ queries to SQL more efficiently. It will remove unneeded elements from the SQL query and bring better performance. |
|||
|
|||
### Table pruning |
|||
|
|||
When you use table-per-hierarchy (TPH) inheritance, previously EF Core generated SQL queries that included JIONs to tables that were not needed. |
|||
|
|||
For example: |
|||
|
|||
```csharp |
|||
public class Order |
|||
{ |
|||
public int Id { get; set; } |
|||
... |
|||
|
|||
public Customer Customer { get; set; } |
|||
} |
|||
|
|||
public class DiscountedOrder : Order |
|||
{ |
|||
public double Discount { get; set; } |
|||
} |
|||
|
|||
public class Customer |
|||
{ |
|||
public int Id { get; set; } |
|||
... |
|||
|
|||
public List<Order> Orders { get; set; } |
|||
} |
|||
|
|||
public class AppContext : DbContext |
|||
{ |
|||
... |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
modelBuilder.Entity<Order>().UseTptMappingStrategy(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Consider the following query to get all customers with at least one order: |
|||
|
|||
```csharp |
|||
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync(); |
|||
``` |
|||
|
|||
Previously, EF Core generated the following SQL query: |
|||
|
|||
```sql |
|||
SELECT [c].[Id], [c].[Name] |
|||
FROM [Customers] AS [c] |
|||
WHERE EXISTS ( |
|||
SELECT 1 |
|||
FROM [Orders] AS [o] |
|||
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id] |
|||
WHERE [c].[Id] = [o].[CustomerId]) |
|||
``` |
|||
|
|||
It included a JOIN to the `DiscountedOrders` table, which was not needed. In EF Core 9, the generated SQL query is: |
|||
|
|||
```sql |
|||
SELECT [c].[Id], [c].[Name] |
|||
FROM [Customers] AS [c] |
|||
WHERE EXISTS ( |
|||
SELECT 1 |
|||
FROM [Orders] AS [o] |
|||
WHERE [c].[Id] = [o].[CustomerId]) |
|||
``` |
|||
|
|||
## EF Core in ABP |
|||
|
|||
ABP Framework is built on top of the latest technologies. It will support EF Core 9 as soon as it is released. You can use the latest features of EF Core in your ABP applications. |
|||
|
|||
For example, you can use the `ExecuteUpdateAsync` method in your ABP application: |
|||
|
|||
```csharp |
|||
public class Book : FullAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
|
|||
public string Author { get; set; } |
|||
} |
|||
|
|||
public class AppContext : AbpDbContext<AppContext> |
|||
{ |
|||
public DbSet<Book> Books { get; set; } |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
builder.Entity<Book>(b => |
|||
{ |
|||
b.ToTable("Books"); |
|||
b.ConfigureByConvention(); |
|||
b.Property(x => x.Name).IsRequired().HasMaxLength(128); |
|||
b.Property(x => x.Author).IsRequired().HasMaxLength(64); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class BookRepository : EfCoreRepository<AppContext, Book, Guid>, IBookRepository |
|||
{ |
|||
public BookRepository(IDbContextProvider<AppContext> dbContextProvider) |
|||
: base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public async Task UpdatePriceByAuthorAsync(string author, float price) |
|||
{ |
|||
await (await GetDbSetAsync()) |
|||
.Where(b => b.Author == author) |
|||
.ExecuteUpdateAsync(b => b.SetProperty(x => x.Price, price)); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* `FullAuditedAggregateRoot` is an aggregate root base class with auditing properties provided by ABP Framework. |
|||
* `IRepository` is a generic repository interface provided by ABP Framework that provides CRUD operations and you can use EF Core's API in your entity repository implementation. |
|||
|
|||
## References |
|||
|
|||
* [LINQ and SQL translation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#linq-and-sql-translation) |
|||
* [ABP Entity Framework Core Integration](https://abp.io/docs/latest/framework/data/entity-framework-core) |
|||
* [ABP Entities](https://abp.io/docs/latest/framework/architecture/domain-driven-design/entities) |
|||
@ -0,0 +1,84 @@ |
|||
# Forwarded Headers |
|||
|
|||
Reverse proxies and load balancers play a crucial role in modern web application architectures. When an application is deployed behind these proxies and load balancers, several specific issues can arise. This document will discuss these issues in detail, explain how ASP.NET Core's forwarded headers middleware can address them, and provide a code example for configuring forwarded headers in an ABP application. |
|||
|
|||
## Possible problem in a Reverse Proxy Environment |
|||
|
|||
When requests pass through a reverse proxy or load balancer, the following common issues can occur: |
|||
|
|||
### 1. Loss of Original Request Information |
|||
|
|||
A reverse proxy or load balancer typically modifies the original HTTP request headers. For example, the proxy may replace the client's `X-Forwarded-For` header, or the `Host` header might be set to the proxy's address. This can result in the backend application being unable to directly access the client's IP address, the true hostname, and the protocol used. |
|||
|
|||
### 2. HTTPS vs HTTP Protocol Confusion |
|||
|
|||
When a request is forwarded by a proxy server, it is often upgraded to HTTPS to ensure secure transmission. The proxy server will send a header like `X-Forwarded-Proto` to indicate whether the original request was HTTP or HTTPS. If the backend application does not correctly handle this header, it may generate URLs with the wrong protocol. |
|||
|
|||
### 3. Path Handling Issues |
|||
|
|||
Since load balancers and proxies might modify or map the request URL paths differently, the backend application could encounter path inconsistencies. For example, a reverse proxy might forward a request from `/api` to `/myapp/api`. If the backend application is not correctly configured, path parsing errors may occur. |
|||
|
|||
### 4. IP Address and Security |
|||
|
|||
Reverse proxies might replace the original client IP address with their own, which can affect logging, authentication, and access control mechanisms. To retrieve the actual client IP address, the `X-Forwarded-For` header must be correctly parsed and trusted. |
|||
|
|||
### 5. Load Balancer Impact |
|||
|
|||
Load balancers might distribute requests to different backend servers using different algorithms. This can create session affinity problems. If session data is stored on a single server and the load balancer directs subsequent requests to different servers, session loss or inconsistency may occur. |
|||
|
|||
## Forwarded Headers Middleware in ABP web application |
|||
|
|||
To resolve the above issues, ASP.NET Core provides a built-in middleware, `ForwardedHeadersMiddleware`, which processes the headers forwarded by reverse proxies. This middleware helps the application recover the correct original request information, such as the client’s IP address, protocol, and host. |
|||
|
|||
### Configuring `ForwardedHeadersMiddleware` |
|||
|
|||
ASP.NET Core’s `ForwardedHeadersMiddleware` supports several HTTP headers: |
|||
|
|||
- **X-Forwarded-For**: Contains the original client’s IP address. |
|||
- **X-Forwarded-Proto**: Indicates whether the original request was HTTP or HTTPS. |
|||
- **X-Forwarded-Host**: Contains the original host requested by the client. |
|||
- **X-Forwarded-Port**: Indicates the original port of the request. |
|||
|
|||
To configure this middleware: |
|||
|
|||
1. In the `ConfigureServices` method of your module, configure the `ForwardedHeadersOptions`: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<ForwardedHeadersOptions>(options => |
|||
{ |
|||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
2. In the `OnApplicationInitialization` method of your module, add the middleware: |
|||
|
|||
> Forwarded Headers Middleware should run before other middleware. This ordering ensures that the middleware relying on forwarded headers information can consume the header values for processing. Forwarded Headers Middleware can run after diagnostics and error handling, but it must be run before calling UseHsts: |
|||
|
|||
```csharp |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var app = context.GetApplicationBuilder(); |
|||
var env = context.GetEnvironment(); |
|||
|
|||
if (env.IsDevelopment()) |
|||
{ |
|||
app.UseDeveloperExceptionPage(); |
|||
app.UseForwardedHeaders(); |
|||
} |
|||
else |
|||
{ |
|||
app.UseErrorPage(); |
|||
app.UseForwardedHeaders(); |
|||
app.UseHsts(); |
|||
} |
|||
|
|||
// Other middleware configurations... |
|||
} |
|||
``` |
|||
|
|||
## References |
|||
|
|||
- [ASP.NET Core Proxy and Load Balancer Configuration](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-9.0) |
|||
@ -0,0 +1,75 @@ |
|||
//Copy from https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.js
|
|||
|
|||
/* Source: https://gist.github.com/lamberta/3768814 |
|||
* Parse a string function definition and return a function object. Does not use eval. |
|||
* @param {string} str |
|||
* @return {function} |
|||
* |
|||
* Example: |
|||
* var f = function (x, y) { return x * y; }; |
|||
* var g = parseFunction(f.toString()); |
|||
* g(33, 3); //=> 99
|
|||
*/ |
|||
function parseFunction(str) { |
|||
if (!str) return void (0); |
|||
|
|||
var fn_body_idx = str.indexOf('{'), |
|||
fn_body = str.substring(fn_body_idx + 1, str.lastIndexOf('}')), |
|||
fn_declare = str.substring(0, fn_body_idx), |
|||
fn_params = fn_declare.substring(fn_declare.indexOf('(') + 1, fn_declare.lastIndexOf(')')), |
|||
args = fn_params.split(','); |
|||
|
|||
args.push(fn_body); |
|||
|
|||
function Fn() { |
|||
return Function.apply(this, args); |
|||
} |
|||
Fn.prototype = Function.prototype; |
|||
|
|||
return new Fn(); |
|||
} |
|||
|
|||
window.onload = function () { |
|||
var configObject = JSON.parse('%(ConfigObject)'); |
|||
var oauthConfigObject = JSON.parse('%(OAuthConfigObject)'); |
|||
|
|||
// Workaround for https://github.com/swagger-api/swagger-ui/issues/5945
|
|||
configObject.urls.forEach(function (item) { |
|||
if (item.url.startsWith("http") || item.url.startsWith("/")) return; |
|||
item.url = window.location.href.replace("index.html", item.url).split('#')[0]; |
|||
}); |
|||
|
|||
// If validatorUrl is not explicitly provided, disable the feature by setting to null
|
|||
if (!configObject.hasOwnProperty("validatorUrl")) |
|||
configObject.validatorUrl = null |
|||
|
|||
// If oauth2RedirectUrl isn't specified, use the built-in default
|
|||
if (!configObject.hasOwnProperty("oauth2RedirectUrl")) |
|||
configObject.oauth2RedirectUrl = (new URL("oauth2-redirect.html", window.location.href)).href; |
|||
|
|||
// Apply mandatory parameters
|
|||
configObject.dom_id = "#swagger-ui"; |
|||
configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]; |
|||
configObject.layout = "StandaloneLayout"; |
|||
|
|||
// Parse and add interceptor functions
|
|||
var interceptors = JSON.parse('%(Interceptors)'); |
|||
if (interceptors.RequestInterceptorFunction) |
|||
configObject.requestInterceptor = parseFunction(interceptors.RequestInterceptorFunction); |
|||
if (interceptors.ResponseInterceptorFunction) |
|||
configObject.responseInterceptor = parseFunction(interceptors.ResponseInterceptorFunction); |
|||
|
|||
if (configObject.plugins) { |
|||
configObject.plugins = configObject.plugins.map(eval); |
|||
} |
|||
|
|||
// Begin Swagger UI call region
|
|||
|
|||
const ui = abp.SwaggerUIBundle(configObject); |
|||
|
|||
ui.initOAuth(oauthConfigObject); |
|||
|
|||
// End Swagger UI call region
|
|||
|
|||
window.ui = ui |
|||
} |
|||
@ -1,6 +1,6 @@ |
|||
{ |
|||
"sdk": { |
|||
"version": "9.0.100-rc.2.24474.11", |
|||
"version": "9.0.100", |
|||
"rollForward": "latestFeature" |
|||
} |
|||
} |
|||
|
|||
@ -1,8 +1,13 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Volo.CmsKit.Admin.Blogs; |
|||
|
|||
public interface IBlogAdminAppService : ICrudAppService<BlogDto, Guid, BlogGetListInput, CreateBlogDto, UpdateBlogDto> |
|||
{ |
|||
Task<ListResultDto<BlogDto>> GetAllListAsync(); |
|||
|
|||
Task MoveAllBlogPostsAsync(Guid blogId, Guid? assignToBlogId = null); |
|||
} |
|||
|
|||
@ -0,0 +1,59 @@ |
|||
@page |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@using Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs |
|||
@using Volo.CmsKit.Localization |
|||
@model DeleteBlogModal |
|||
@inject IHtmlLocalizer<CmsKitResource> L |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
|
|||
<form method="post" asp-page="/CmsKit/Blogs/DeleteBlogModal" autocomplete="off"> |
|||
@{ |
|||
var deleteAllClicked = "checked"; |
|||
var deleteButtonDisabled = ""; |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["AreYouSure"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
|
|||
<abp-input asp-for="Blog.Id" type="hidden"></abp-input> |
|||
|
|||
<p class="fw-bold">@L.GetString("BlogDeletionConfirmationMessage", Model.Blog.Name).Value</p> |
|||
|
|||
@if (Model.Blog.BlogPostCount > 0) |
|||
{ |
|||
<p class="mt-2">@L.GetString("ChooseAnActionForBlog", Model.Blog.BlogPostCount).Value</p> |
|||
|
|||
|
|||
if (Model.Blog.OtherBlogs.Any()) |
|||
{ |
|||
deleteAllClicked = ""; |
|||
deleteButtonDisabled = "disabled"; |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="radio" checked name="assign" id="assign"> |
|||
<label class="form-check-label" for="assign">@L["AssignBlogPostsToOtherBlog"].Value</label> |
|||
</div> |
|||
<select name="Blog.AssignToBlogId" id="Blog_AssignToBlogId" class="form-select mt-2"> |
|||
<option value="" selected>@L["SelectAnBlogToAssign"].Value</option> |
|||
@foreach (var blog in Model.Blog.OtherBlogs) |
|||
{ |
|||
<option value="@blog.Key">@blog.Value</option> |
|||
} |
|||
</select> |
|||
} |
|||
|
|||
<div class="form-check mt-2"> |
|||
<input class="form-check-input" type="radio" @deleteAllClicked name="assign" id="deleteAll"> |
|||
<label class="form-check-label" for="deleteAll">@L["DeleteAllBlogPostsOfThisBlog"].Value</label> |
|||
</div> |
|||
} |
|||
</abp-modal-body> |
|||
<abp-modal-footer> |
|||
<button class="btn btn-outline-danger" data-bs-dismiss="modal" type="button">@L["Cancel"]</button> |
|||
<button class="btn btn-danger" @deleteButtonDisabled type="submit"><i class="fa fa-trash"></i> <span>@L["Delete"]</span></button> |
|||
</abp-modal-footer> |
|||
</abp-modal> |
|||
} |
|||
|
|||
</form> |
|||
@ -0,0 +1,57 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
using Volo.Abp.ObjectExtending; |
|||
using Volo.CmsKit.Admin.Blogs; |
|||
|
|||
namespace Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs; |
|||
|
|||
public class DeleteBlogModal : CmsKitAdminPageModel |
|||
{ |
|||
[BindProperty] |
|||
public BlogInfoModel Blog { get; set; } |
|||
|
|||
protected IBlogAdminAppService BlogAdminAppService { get; } |
|||
|
|||
public DeleteBlogModal(IBlogAdminAppService blogAdminAppService) |
|||
{ |
|||
BlogAdminAppService = blogAdminAppService; |
|||
} |
|||
|
|||
public virtual async Task OnGetAsync(Guid id) |
|||
{ |
|||
var blog = await BlogAdminAppService.GetAsync(id); |
|||
var allBlogs = await BlogAdminAppService.GetAllListAsync(); |
|||
|
|||
Blog = new BlogInfoModel |
|||
{ |
|||
Id = blog.Id, |
|||
Name = blog.Name, |
|||
BlogPostCount = blog.BlogPostCount, |
|||
OtherBlogs = allBlogs.Items.Where(b => b.Id != blog.Id).Select(e => new KeyValuePair<Guid, string>(e.Id, e.Name)).ToList() |
|||
}; |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await BlogAdminAppService.MoveAllBlogPostsAsync(Blog.Id, Blog.AssignToBlogId); |
|||
await BlogAdminAppService.DeleteAsync(Blog.Id); |
|||
return NoContent(); |
|||
} |
|||
|
|||
public class BlogInfoModel : ExtensibleObject |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
public int BlogPostCount { get; set; } |
|||
|
|||
public List<KeyValuePair<Guid, string>> OtherBlogs { get; set; } |
|||
|
|||
public Guid? AssignToBlogId { get; set; } |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue