@ -0,0 +1,124 @@ |
|||
# EF Core 8 - Enhancements to JSON column mapping |
|||
|
|||
In this article, we will examine the enhancements introduced in EF Core 8 for the JSON column feature, building upon the foundation laid by [JSON columns in Entity Framework Core 7](https://community.abp.io/posts/json-columns-in-entity-framework-core-7-cjaom76j). |
|||
|
|||
## The entity classes we will be using in the article |
|||
|
|||
```csharp |
|||
public class Person |
|||
{ |
|||
public int Id { get; set; } |
|||
[Required] |
|||
public string Name { get; set; } |
|||
[Required] |
|||
public ContactDetails ContactDetails { get; set; } |
|||
} |
|||
|
|||
public class ContactDetails |
|||
{ |
|||
public List<Address> Addresses { get; set; } = new(); |
|||
public string? Phone { get; set; } |
|||
} |
|||
|
|||
public class Address |
|||
{ |
|||
public Address(string street, string city, string postcode, string country) |
|||
{ |
|||
Street = street; |
|||
City = city; |
|||
Postcode = postcode; |
|||
Country = country; |
|||
} |
|||
|
|||
public string Street { get; set; } |
|||
public string City { get; set; } |
|||
public string Postcode { get; set; } |
|||
public string Country { get; set; } |
|||
public bool IsMainAddress { get; set; } |
|||
} |
|||
``` |
|||
|
|||
## The DbContext class we will be using in the article |
|||
|
|||
```csharp |
|||
public class AppDbContext : DbContext |
|||
{ |
|||
public DbSet<Person> Persons { get; set; } = null!; |
|||
|
|||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |
|||
{ |
|||
#if SQLSERVER |
|||
optionsBuilder.UseSqlServer("Server=localhost;Database=EfCore8Json;Trusted_Connection=True;TrustServerCertificate=True"); |
|||
#elif SQLITE |
|||
optionsBuilder.UseSqlite("Data Source=EfCore8Json.db"); |
|||
#endif |
|||
base.OnConfiguring(optionsBuilder); |
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
modelBuilder.Entity<Person>(b => |
|||
{ |
|||
b.ToTable("Persons"); |
|||
b.HasKey(x => x.Id); |
|||
b.Property(x => x.Name).IsRequired(); |
|||
b.OwnsOne(x => x.ContactDetails, cb => |
|||
{ |
|||
cb.ToJson(); |
|||
cb.Property(x => x.Phone); |
|||
cb.OwnsMany(x => x.Addresses); |
|||
}); |
|||
}); |
|||
|
|||
base.OnModelCreating(modelBuilder); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Translate element access into JSON arrays |
|||
|
|||
EF Core 8 supports indexing in JSON arrays when executing queries. For example, the following query returns individuals whose first address is the main address in the database: |
|||
|
|||
```csharp |
|||
var query = dbContext.Persons |
|||
.Select(x => x.ContactDetails.Addresses[0]) |
|||
.Where(x => x.IsMainAddress == true) |
|||
.ToListAsync(); |
|||
``` |
|||
|
|||
The generated SQL query is as follows when using SQL Server: |
|||
|
|||
```sql |
|||
SELECT JSON_QUERY([p].[ContactDetails], '$.Addresses[0]'), [p].[Id] |
|||
FROM [Persons] AS [p] |
|||
WHERE CAST(JSON_VALUE([p].[ContactDetails], '$.Addresses[0].IsMainAddress') AS bit) = CAST(1 AS bit) |
|||
``` |
|||
|
|||
> Note: If you attempt to access an index that is outside of the array, it will return null. |
|||
|
|||
## JSON Columns for SQLite |
|||
|
|||
In EF Core 7, JSON column mapping was supported for Azure SQL/SQL Server. In EF Core 8, this support has been extended to include SQLite as well. |
|||
|
|||
### Queries into JSON columns |
|||
|
|||
The following query returns individuals whose first address is the main address in the database: |
|||
|
|||
```csharp |
|||
var query = dbContext.Persons |
|||
.Select(x => x.ContactDetails.Addresses[0]) |
|||
.Where(x => x.IsMainAddress == true) |
|||
.ToListAsync(); |
|||
``` |
|||
|
|||
The generated SQL query is as follows when using SQLite: |
|||
|
|||
```sql |
|||
SELECT "p"."ContactDetails" ->> '$.Addresses[0]', "p"."Id" |
|||
FROM "Persons" AS "p" |
|||
WHERE "p"."ContactDetails" ->> '$.Addresses[0].IsMainAddress' = 0 |
|||
``` |
|||
|
|||
## References |
|||
|
|||
- [EF Core 8 - Enhancements to JSON column mapping](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#enhancements-to-json-column-mapping) |
|||
@ -0,0 +1,156 @@ |
|||
# New Minimal APIs features in ASP.NET Core 8.0 |
|||
|
|||
In this article, we will see the new features of Minimal APIs in ASP.NET Core 8.0. |
|||
|
|||
## Binding to forms |
|||
|
|||
We can bind to forms using the [FromForm] attribute. Let's see an example: |
|||
|
|||
```csharp |
|||
app.MapPost("/books", async ([FromForm] string name, |
|||
[FromForm] BookType bookType, IFormFile? cover, BookDb db) => |
|||
{ |
|||
var book = new Book |
|||
{ |
|||
Name = name, |
|||
BookType = bookType |
|||
}; |
|||
|
|||
if (cover is not null) |
|||
{ |
|||
var coverName = Path.GetRandomFileName(); |
|||
|
|||
using var stream = File.Create(Path.Combine("wwwroot", coverName)); |
|||
await cover.CopyToAsync(stream); |
|||
book.Cover = coverName; |
|||
} |
|||
|
|||
db.Books.Add(book); |
|||
await db.SaveChangesAsync(); |
|||
|
|||
return Results.Ok(); |
|||
}); |
|||
``` |
|||
|
|||
Another way is using the [AsParameters] attribute, the following code binds from form values to properties of the `NewBookRequest` record struct: |
|||
|
|||
```csharp |
|||
public record NewBookRequest([FromForm] string Name, [FromForm] BookType BookType, IFormFile? Cover); |
|||
|
|||
app.MapPost("/books", async ([AsParameters] NewBookRequest request, BookDb db) => |
|||
{ |
|||
var book = new Book |
|||
{ |
|||
Name = request.Name, |
|||
BookType = request.BookType |
|||
}; |
|||
|
|||
if (request.Cover is not null) |
|||
{ |
|||
var coverName = Path.GetRandomFileName(); |
|||
|
|||
using var stream = File.Create(Path.Combine("wwwroot", coverName)); |
|||
await request.Cover.CopyToAsync(stream); |
|||
book.Cover = coverName; |
|||
} |
|||
|
|||
db.Books.Add(book); |
|||
await db.SaveChangesAsync(); |
|||
|
|||
return Results.Ok(); |
|||
}); |
|||
``` |
|||
|
|||
## Antiforgery |
|||
|
|||
ASP.NET Core 8.0 adds support for antiforgery tokens. We can call the `AddAntiforgery` method to register the antiforgery services and `WebApplicationBuilder` will automatically add the antiforgery middleware to the pipeline: |
|||
|
|||
```csharp |
|||
var builder = WebApplication.CreateBuilder(); |
|||
|
|||
builder.Services.AddAntiforgery(); |
|||
|
|||
var app = builder.Build(); |
|||
|
|||
// Implicitly added by WebApplicationBuilder if AddAntiforgery is called. |
|||
// app.UseAntiforgery(); |
|||
|
|||
app.MapGet("/", () => "Hello World!"); |
|||
|
|||
app.Run(); |
|||
``` |
|||
|
|||
Example of using antiforgery tokens: |
|||
|
|||
```csharp |
|||
|
|||
// Use the antiforgery service to generate tokens. |
|||
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) => |
|||
{ |
|||
var token = antiforgery.GetAndStoreTokens(context); |
|||
return Results.Content(...., "text/html"); |
|||
}); |
|||
|
|||
// It will automatically validate the token. |
|||
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo)); |
|||
|
|||
// Disable antiforgery validation for this endpoint. |
|||
app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo)) |
|||
.DisableAntiforgery(); |
|||
``` |
|||
|
|||
|
|||
## Native AOT |
|||
|
|||
ASP.NET Core 8.0 adds support for Native AOT. |
|||
|
|||
You can add `PublishAot` to the project file to enable Native AOT: |
|||
|
|||
```xml |
|||
<PropertyGroup> |
|||
<PublishAot>true</PublishAot> |
|||
</PropertyGroup> |
|||
``` |
|||
|
|||
### Web API (native AOT) template |
|||
|
|||
You can use the `dotnet new webapiaot` command to create a new Minimal APIs project with Native AOT enabled. |
|||
|
|||
```diff |
|||
+using System.Text.Json.Serialization; |
|||
|
|||
-var builder = WebApplication.CreateBuilder(); |
|||
+var builder = WebApplication.CreateSlimBuilder(args); |
|||
|
|||
+builder.Services.ConfigureHttpJsonOptions(options => |
|||
+{ |
|||
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); |
|||
+}); |
|||
|
|||
var app = builder.Build(); |
|||
|
|||
var sampleTodos = TodoGenerator.GenerateTodos().ToArray(); |
|||
|
|||
var todosApi = app.MapGroup("/todos"); |
|||
todosApi.MapGet("/", () => sampleTodos); |
|||
todosApi.MapGet("/{id}", (int id) => |
|||
sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo |
|||
? Results.Ok(todo) |
|||
: Results.NotFound()); |
|||
|
|||
app.Run(); |
|||
|
|||
+[JsonSerializable(typeof(Todo[]))] |
|||
+internal partial class AppJsonSerializerContext : JsonSerializerContext |
|||
+{ |
|||
+ |
|||
+} |
|||
``` |
|||
|
|||
* Reflection isn't supported in native AOT, you must use the `JsonSerializable` attribute to specify the types that you want to serialize/deserialize. |
|||
|
|||
## References |
|||
|
|||
* https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#minimal-apis |
|||
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#explicit-binding-from-form-values |
|||
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0 |
|||
@ -0,0 +1,138 @@ |
|||
# ASP.NET 8: What's New About Authentication and Authorization |
|||
|
|||
In ASP.NET 8, the concept of authentication and authorization is undergoing a transformation. Specifically, ASP.NET Core Identity is transitioning from a focus on traditional web pages to a more API-driven approach. We will see what the Identity API endpoints are, why we need them, and how to use them in detail. So, let's dive in. |
|||
|
|||
## What is ASP.NET Core Identity? |
|||
|
|||
ASP.NET Core Identity is like an extra toolkit that comes with ASP.NET Core. It offers a bunch of services to help you deal with user accounts in your ASP.NET Core application. These services include both basic concepts and ready-made solutions for each task. |
|||
|
|||
With ASP.NET Core Identity, you can save user accounts in your app, handle user information, add extra security with two-factor authentication, and even connect other log in options, like social media logins, to user accounts. In simple terms, it's all about keeping user accounts in your app and making it easy for users to log in. |
|||
|
|||
 |
|||
|
|||
Prior to ASP.NET 8, there were limitations on using ASP.NET Identity in Single Page Applications (SPAs). Because the ASP.NET Core Identity framework is primarily tailored for traditional server-rendered web applications, like ASP.NET Core MVC or Razor Pages apps. However, the Identity API endpoints that come with ASP.NET 8 aim to solve token-based authentication and authorization in SPA without external dependencies. |
|||
|
|||
## Why do we need Identity APIs? |
|||
|
|||
The introduction of Identity endpoints in ASP.NET 8 aims to streamline the process of integrating ASP.NET Core Identity into both API server applications and front-end SPAs like those built with JavaScript or Blazor. To understand the importance of these endpoints, let's first examine the disadvantages of using ASP.NET Core Identity before ASP.NET 8. |
|||
|
|||
Let's delve into a common scenario: |
|||
|
|||
Imagine you have a straightforward application composed of an ASP.NET Core backend that exposes APIs. On the client side, you have a SPA application that communicates with these APIs. Now, you want to incorporate user accounts, complete with authentication and authorization, into your application. |
|||
|
|||
Before the advent of the identity endpoints, you could add ASP.NET Core Identity and the default Razor Pages UI to your app by adding a few packages, updating your database schema, and registering some services. However, this approach had some disadvantages. From a user experience perspective, the full-page refreshes of Razor Pages can be a major drawback especially compared to the fluidity of SPAs. On the other hand, from a developer's perspective, if you want the default pages to be compatible with the rest of your application, you may need to update more than 30 pages. Moreover, the default Razor Pages UI uses traditional cookie-based authentication, not bearer tokens. |
|||
|
|||
In ASP.NET 8, identity endpoints were introduced to simplify the process of adding user accounts to ASP.NET Core API apps used with SPAs or mobile. These endpoints streamline the user management process by providing an alternative to the traditional Razor Page Identity UI pages. Additionally, a new endpoint is included for retrieving bearer tokens, which can be used for authentication. These changes directly address the concerns mentioned earlier, making it easier to create a user-friendly and integrated experience within your app, with no full-page refreshes or styling conflicts. |
|||
|
|||
## How to add Identity APIs? |
|||
|
|||
In this part, I'll create a demo application, add all of the required packages, and lastly map the Identity APIs. |
|||
|
|||
Create a new ASP.NET Core app with the following command and change the directory to the created application folder: |
|||
|
|||
```bash |
|||
dotnet new webapi --name NewIdentityEndpoints -o IdentityDemo |
|||
cd IdentityDemo |
|||
``` |
|||
|
|||
After that, open the application in your favorite IDE and add the required packages: |
|||
|
|||
```csharp |
|||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0-rc.2.23480.2" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0-rc.2.23480.1" /> |
|||
``` |
|||
|
|||
**Note:** I used the `.NET 8.0.100-rc.2-**` SDK for everything in this post. |
|||
|
|||
Then replace `Program.cs` as below: |
|||
|
|||
```csharp |
|||
using System.Security.Claims; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore; |
|||
|
|||
var builder = WebApplication.CreateBuilder(args); |
|||
|
|||
builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); |
|||
builder.Services.AddAuthorizationBuilder(); |
|||
|
|||
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase("AppDb")); |
|||
|
|||
builder.Services.AddIdentityCore<MyCustomUser>() |
|||
.AddEntityFrameworkStores<ApplicationDbContext>() |
|||
.AddApiEndpoints(); |
|||
|
|||
builder.Services.AddEndpointsApiExplorer(); |
|||
builder.Services.AddSwaggerGen(); |
|||
|
|||
var app = builder.Build(); |
|||
|
|||
app.MapGroup("/my-identity-api").MapIdentityApi<IdentityUser>(); |
|||
|
|||
app.MapGet("/", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); |
|||
|
|||
if (app.Environment.IsDevelopment()) |
|||
{ |
|||
app.UseSwagger(); |
|||
app.UseSwaggerUI(); |
|||
} |
|||
|
|||
app.Run(); |
|||
|
|||
class MyCustomUser : IdentityUser { } |
|||
|
|||
class ApplicationDbContext : IdentityDbContext<MyCustomUser> |
|||
{ |
|||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } |
|||
} |
|||
|
|||
``` |
|||
|
|||
When you run the application, you will see a result as the following: |
|||
|
|||
 |
|||
|
|||
## Custom Authorization Policies |
|||
|
|||
Prior to ASP.NET 8, adding a parameterized authorization policy to an endpoint required writing a lot of code. |
|||
|
|||
- Implementing an `AuthorizeAttribute` for each policy |
|||
- Implementing an `AuthorizationPolicyProvider` to process a custom policy from a string-based contract |
|||
- Implementing an `AuthorizationRequirement` for the policy |
|||
- Implementing an `AuthorizationHandler` for each requirement |
|||
|
|||
However, implementing the `IAuthorizationRequirementData` interface that comes with ASP.NET 8, your application code should look like this: |
|||
|
|||
```csharp |
|||
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData |
|||
{ |
|||
public MinimumAgeAuthorizeAttribute(int age) => Age =age; |
|||
public int Age { get; } |
|||
|
|||
public IEnumerable<IAuthorizationRequirement> GetRequirements() |
|||
{ |
|||
yield return this; |
|||
} |
|||
} |
|||
|
|||
class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeAuthorizeAttribute> |
|||
{ |
|||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement) |
|||
{ |
|||
... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
See [here](https://gist.github.com/captainsafia/7c54e92d12df695ff0908e989fb8531f) for the complete code. |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I've shown you the Identity APIs introduced with ASP.NET 8, why we need them, and how we can add them to our application, and finally I explained how to define a custom authorization policy with fewer lines of code. |
|||
|
|||
## References |
|||
- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#authentication-and-authorization |
|||
- https://auth0.com/blog/whats-new-dotnet8-authentication-authorization/ |
|||
- https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/ |
|||
- https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/ |
|||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 90 KiB |
@ -0,0 +1,100 @@ |
|||
# .NET 8: Serialization Improvements |
|||
|
|||
The [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) namespace provides functionality for serializing to and deserializing from JSON. In .NET 8, there have been many improvements to the System.Text.Json serialization and deserialization functionality. |
|||
|
|||
## Source generator |
|||
|
|||
.NET 8 includes enhancements of the System.Text.Json source generator that are aimed at making the Native AOT experience on par with the reflection-based serializer. This is important because now you can select the source generator for System.Text.Json. To see a comparison of Reflection versus source generation in System.Text.Json, check out the comparison [documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation?pivots=dotnet-8-0#reflection). |
|||
|
|||
## Interface hierarchies |
|||
|
|||
.NET 8 adds support for serializing properties from interface hierarchies. Here is a code sample that shows this feature. |
|||
|
|||
```csharp |
|||
ICar car = new MyCar { Color = "Red", NumberOfWheels = 4 }; |
|||
JsonSerializer.Serialize(car); // {"Color":"Red","NumberOfWheels":4} |
|||
|
|||
public interface IVehicle |
|||
{ |
|||
public string Color { get; set; } |
|||
} |
|||
|
|||
public interface ICar : IVehicle |
|||
{ |
|||
public int NumberOfWheels { get; set; } |
|||
} |
|||
|
|||
public class MyCar : ICar |
|||
{ |
|||
public string Color { get; set; } |
|||
public int NumberOfWheels { get; set; } |
|||
} |
|||
``` |
|||
|
|||
## Naming policies |
|||
|
|||
Two new naming policies `snake_case` and `kebab-case` have been added. You can use them as shown below: |
|||
|
|||
```csharp |
|||
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; |
|||
JsonSerializer.Serialize(new { CreationTime = new DateTime(2023,11,6) }, options); // {"creation_time":"2023-11-06T00:00:00"} |
|||
``` |
|||
|
|||
## Read-only properties |
|||
|
|||
With .NET 8, you can deserialize onto read-only fields or properties. This feature can be globally enabled by setting `PreferredObjectCreationHandling` to `JsonObjectCreationHandling.Populate`. You can also enable this feature for a class or one of its members by adding the `[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]` attribute. Here is a sample: |
|||
|
|||
```csharp |
|||
using System.Text.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
var book = JsonSerializer.Deserialize<Book>("""{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}}""")!; |
|||
Console.WriteLine(JsonSerializer.Serialize(book)); |
|||
|
|||
class Author |
|||
{ |
|||
public required string Name { get; set; } |
|||
} |
|||
|
|||
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] |
|||
class Book |
|||
{ |
|||
// Both of these properties are read-only. |
|||
public List<string> Contributors { get; } = new(); |
|||
public Author Author { get; } = new() {Name = "Undefined"}; |
|||
} |
|||
``` |
|||
|
|||
Before .NET 8, the output was like this: |
|||
|
|||
```json |
|||
{"Contributors":[],"Author":{"Name":"Undefined"}} |
|||
``` |
|||
|
|||
With .NET 8, the output now looks like this: |
|||
|
|||
```json |
|||
{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}} |
|||
``` |
|||
|
|||
## Disable reflection-based default |
|||
|
|||
One of the nice features about Serialization is, now you can disable using the reflection-based serializer by default. To disable default reflection-based serialization, set the `JsonSerializerIsReflectionEnabledByDefault` MSBuild property to `false` in your project file. |
|||
|
|||
## Streaming deserialization APIs |
|||
|
|||
.NET 8 includes new `IAsyncEnumerable<T>` streaming deserialization extension methods. The new extension methods invoke streaming APIs and return `IAsyncEnumerable<T>`. Here is a sample code that uses this new feature. |
|||
|
|||
```csharp |
|||
const string RequestUri = "https://yourwebsite.com/api/saas/tenants?skipCount=0&maxResultCount=10"; |
|||
using var client = new HttpClient(); |
|||
IAsyncEnumerable<Tenant> tenants = client.GetFromJsonAsAsyncEnumerable<Tenant>(RequestUri); |
|||
|
|||
await foreach (Tenant tenant in tenants) |
|||
{ |
|||
Console.WriteLine($"* '{tenant.name}' uses '{tenant.editionName |
|||
}' edition"); |
|||
} |
|||
``` |
|||
|
|||
I have mentioned some of the items for Serialization Improvements in .NET 8. If you want to check the full list, you can read it in the .NET 8's [What's new document](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#serialization). |
|||
@ -0,0 +1,89 @@ |
|||
# Blazor's History and Full-stack Web UI |
|||
|
|||
Blazor is a web framework that allows developers to build interactive web applications using .NET instead of JavaScript. The first version of Blazor was **released on May 14, 2020**. Since its initial release, Blazor has evolved with the new versions. Until now, six different versions have been declared. Sometimes, it can be not very clear to see the differences between these approaches. First, let's try to understand these. |
|||
|
|||
* **Blazor-Server**: >> *Loads fast at first* >> In this version, heavy things are done in the server. Browsers are thin clients and download a small page for the first load. The page updates are done via SignalR connection. This was released with .NET Core 3. |
|||
* **Blazor WebAssembly (WASM):** >> *Loads slow at first* >> In this version, some binary files are being downloaded to the browser. This approach takes longer initialization time than the "Server" approach. The hard work is done on the browser. |
|||
* **Blazor Hybrid:** It's a combination of Blazor and Xamarin.Forms. It allows you to run your app on iOS, Android, macOS, and Windows. Blazor Hybrid uses a WebView component to host the Blazor-based user interface within the mobile app. |
|||
* **Blazor Native**: It runs natively on the devices and uses a common UI abstraction to render native controls for that device. This is very similar to how frameworks like Xamarin Forms or React Native work today. It has also been considered but has not reached [the planning stage](https://devblogs.microsoft.com/dotnet/blazor-server-in-net-core-3-0-scenarios-and-performance/). |
|||
|
|||
* **Blazor United**: Recently, Microsoft updated this name to "Fullstack web UI". Blazor-Server and Blazor WASM both have some disadvantages and advantages. So, Microsoft decided to combine these two approaches and find an optimum solution for the entire Blazor version. We can call it *Best of Blazor* 😀 |
|||
|
|||
## Why is "Blazor Fullstack Web UI" the best? |
|||
|
|||
These apps are a combination of both Blazor Server and Blazor WASM. It provides the advantages of Blazor Server and WASM. Developers would be able to more fine-tune the rendering mode. **This approach overcomes the large binary downloads of Blazor WASM, and it resolves the Blazor Server's problem, which always needs to be connected to the server via SignalR.** |
|||
|
|||
> Blazor Fullstack Web UI comes with the .NET 8, and it will be published on November 14, 2023. |
|||
|
|||
I took the following photo from Steven Sanderson's talk at NDC Porto 2023. You can read my impressions of this conf at https://volosoft.com/blog/ndc-port-2023-impressions, but after reading this one. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
There are two basic page styles: |
|||
|
|||
* **HTML**; simple and loads fast! Eg: MVC, Razor Pages |
|||
* **Single Page Apps (SPA):** high interaction with the client and loads slower! Eg: Blazor WASM, Blazor Server... |
|||
|
|||
## HTML + SPA => Blazor Fullstack |
|||
|
|||
Blazor Fullstack (formerly United) is a technology that turns Blazor Server into a SPA style. |
|||
|
|||
.NET 8 will combine Blazor Server's server-side rendering and WebAssembly's client-side interaction. |
|||
|
|||
You can switch between two rendering modes and even mix them on the same page. With .NET 8 there also comes amazing features like; |
|||
|
|||
* [Streaming rendering](https://github.com/dotnet/aspnetcore/issues/46352): With this feature, most of the page will be rendered, and long async operations on the server will still be in progress. |
|||
|
|||
* [Progressive enhancement of form submission & navigation](https://github.com/dotnet/aspnetcore/issues/46399): With this feature, it doesn't fully reload the page after submitting the form. This gives the user a better and smoother experience. |
|||
|
|||
|
|||
|
|||
## How it works? |
|||
|
|||
### Rendering on Server |
|||
|
|||
You can add `WebComponentRenderMode.Server` to your Blazor components so that these components will run interactively. In the below example, the list editor will make AJAX requests to the server like single-page applications. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
And sure you can add `WebComponentRenderMode.Server` to your page level, and the complete page will be rendered as a server component. All inputs on this page can work as an interactive server component like SPA mode. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
### Rendering on client |
|||
|
|||
You can switch to WebAssembly mode by writing `WebComponentRenderMode.WebAssembly` attribute to your page. By doing so, the whole page should run interactively using WebAssembly. This time there's no server connection anymore because it loads the binaries (WebAssembly runtimes) at the page load. |
|||
|
|||
 |
|||
|
|||
## How it works? |
|||
|
|||
To enable Blazor Full-stack Web UI, you need to write `net8.0;net7.0-browser` into the `TargetFrameworks` area of your `csproj` file. These two keywords change your app like this; `net8.0` framework renders on the server, and `net7.0-browser` framework renders on the browser. |
|||
|
|||
 |
|||
|
|||
## Let the System decide on WebAssembly or Server approach |
|||
|
|||
You can let the system decide whether it uses `WebAssembly` or `Server`. This can be done with the `Auto` mode of the `WebComponentRenderMode`. In this case, it will not load binary files (WebAssembly files) for the initial page that has `WebComponentRenderMode.Server` attribute, but whenever the user navigates to a page that has `WebComponentRenderMode.WebAssembly`, it will download the runtimes. This will allow us to load the initial page very fast, and when we need interactivity, we can switch to `WebAssembly` and wait for the binaries to download. But this download will be done one time because it will be cached. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
## Conclusion |
|||
|
|||
I summarized the new generation Blazor in a very simple way. This architecture will be useful to everyone who uses Blazor. |
|||
|
|||
|
|||
|
|||
*Resources:* |
|||
|
|||
* You can check out Dan Roth's GitHub issue 👉 [github.com/dotnet/aspnetcore/issues/46636](https://github.com/dotnet/aspnetcore/issues/46636). |
|||
* Steven Sanderson's YouTube video is very good for understanding these concepts 👉 [Blazor United Prototype Video](https://youtu.be/48G_CEGXZZM). |
|||
|
|||
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 103 KiB |
@ -0,0 +1,79 @@ |
|||
# HierarchyId Support in Entity Framework Core |
|||
|
|||
Entity Framework Core has official support for HierarchyId, which allows you to store and query hierarchical data in SQL Server databases. Hierarchical data is a common data structure found in many applications. Whether you are dealing with organizational structures, product categories, or threaded discussions, handling hierarchies efficiently is crucial. In this blog post, we will explore how to manage hierarchical data using Entity Framework Core (EF Core) in combination with HierarchyId. |
|||
|
|||
## How to use HierarchyId in EF Core |
|||
|
|||
To use HierarchyId in EF Core, you need to install the [Microsoft.EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.HierarchyId/) NuGet package. This package contains query and update support for HierarchyId. |
|||
|
|||
To configure EF Core to use HierarchyId, you need to call the `UseHierarchyId` method when configuring the SQL Server provider. |
|||
|
|||
```csharp |
|||
options.UseSqlServer( |
|||
connectionString, |
|||
x => x.UseHierarchyId()); |
|||
``` |
|||
|
|||
## Modeling Hierarchies |
|||
|
|||
The `HierarchyId` type can be used for properties of an entity type. For example, assume we want to model the personnel in an organization. Each person has a name and a manager. The manager is also a person. We can model this using the following entity type: |
|||
|
|||
```csharp |
|||
public class Person |
|||
{ |
|||
public int Id { get; set; } |
|||
public string Name { get; set; } |
|||
public HierarchyId Manager { get; set; } |
|||
} |
|||
``` |
|||
|
|||
Each person can be traced from the patriarch down the tree using its `Manager` property. SQL Server uses a compact binary format for these paths, but it is common to parse to and from a human-readable string representation when working with code. In this representation, the position at each level is separated by a `/` character. |
|||
|
|||
 |
|||
|
|||
## Querying Hierarchies |
|||
|
|||
HierarchyId exposes several methods that can be used in LINQ queries. |
|||
|
|||
* `GetAncestor(int level)` returns the ancestor at the specified level. |
|||
|
|||
* `GetDescendant(HierarchyId? child1, HierarchyId? child2)` returns the descendant at the specified level. |
|||
|
|||
* `GetLevel()` returns the level of the node in the hierarchy. |
|||
|
|||
* `GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot)` returns the node with a new parent. |
|||
|
|||
* `IsDescendantOf(HierarchyId? parent)` returns whether the node is a descendant of the specified node. |
|||
|
|||
Example: |
|||
|
|||
```csharp |
|||
// Get entities at a given level in the tree |
|||
var generation = await context.Persons.Where(x => x.Manager.GetLevel() == level).ToListAsync(); |
|||
|
|||
// Get the direct ancestor of an entity |
|||
var parent = await context.Persons.Where(x => x.Manager.GetAncestor(1) == manager).SingleAsync(); |
|||
``` |
|||
|
|||
For example, your company may want to change the manager of an organizational unit. To do this, you can use the `GetReparentedValue` method to update the manager for all descendants of the organizational unit. |
|||
|
|||
```csharp |
|||
var newManager = await context.Persons.SingleAsync(x => x.Id == newManagerId); |
|||
|
|||
var descendants = await context.Persons |
|||
.Where(x => x.Manager.IsDescendantOf(oldManager)) |
|||
.ToListAsync(); |
|||
|
|||
foreach (var descendant in descendants) |
|||
{ |
|||
descendant.Manager = descendant.Manager.GetReparentedValue(oldManager.Manager, newManager.Manager); |
|||
} |
|||
|
|||
await context.SaveChangesAsync(); |
|||
``` |
|||
|
|||
## References |
|||
|
|||
For more information about hierarchy id, see the following resource: |
|||
|
|||
https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#hierarchyid-in-net-and-ef-core |
|||
|
After Width: | Height: | Size: 214 KiB |
@ -0,0 +1,75 @@ |
|||
# Native AOT Compilation in .NET 8 |
|||
Native AOT (Ahead-of-Time) compilation is a feature that allows developers to create a self-contained app compiled to native code that can run on machines without the .NET runtime installed. It results in benefits such as minimized disk footprint, reduced executable size, reduced startup time, and reduced memory demand. |
|||
|
|||
Native AOT compilation isn't a new feature in .NET 8. It's first introduced in .NET 7. |
|||
|
|||
|
|||
Differences between the AOT Compilation of .NET 7 and .NET 8 are: |
|||
|
|||
|
|||
- **System.Text.Json improvements**: .NET 8 adds support for more types, source generation, interface hierarchies, naming policies, read-only properties, and more. |
|||
- **New types for performance**: .NET 8 introduces new types such as FrozenDictionary, FrozenSet, SearchValues, CompositeFormat, TimeProvider, and ITimer to improve the app performance. |
|||
- **System.Numerics and System.Runtime.Intrinsics enhancements**: .NET 8 adds support for Vector512, AVX-512, IUtf8SpanFormattable, Lerp, and more. |
|||
- **System.ComponentModel.DataAnnotations additions**: .NET 8 adds new data validation attributes for cloud-native services and a new ValidateOptionsResultBuilder type. |
|||
- **Hosted services lifecycle methods**: .NET 8 adds new methods such as StartAsync, StopAsync, StartBackgroundAsync, and StopBackgroundAsync for hosted services. |
|||
|
|||
It's important to note that not all features in ASP.NET Core are currently compatible with native AOT. For more information, see [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). |
|||
|
|||
## How to use Native AOT Compilation in .NET 8 |
|||
|
|||
You can add `<PublishAot>true</PublishAot>` in your project .csproj file to enable Native AOT Compilation. |
|||
|
|||
- For the new projects, you can create them with the `--aot` parameter. Example: `dotnet new console --aot`. |
|||
|
|||
By default, the compiler chooses a blended approach code optimization but you can specify an optimization preference inside your .csproj file. You can choose **size** or **speed** according your requirements. |
|||
|
|||
```xml |
|||
<OptimizationPreference>Size</OptimizationPreference> |
|||
``` |
|||
|
|||
or |
|||
|
|||
```xml |
|||
<OptimizationPreference>Speed</OptimizationPreference> |
|||
``` |
|||
|
|||
### Results |
|||
|
|||
I have created a simple console application to test the Native AOT Compilation. I have used a simple console application that writes "Hello World!" to the console 100 times. I have tested the application with different optimization preferences. I have used the following results: |
|||
|
|||
|
|||
| | Size | Speed | |
|||
| --- | --- | --- | |
|||
| .NET 8 <br/>_(Self-Contained, Single File)_ | 65938 kb | 00.0051806 ~5ms | |
|||
| .NET 7 AOT (default) | 4452 kb | 00.0029823 ~2ms | |
|||
| .NET 8 AOT (default) | 1242 kb | 00.0028638 ~2ms | |
|||
| AOT (Speed)| 1280 kb | 00.0023838 ~2ms | |
|||
| AOT (Size) | 1111 kb | 00.0025145 ~2ms | |
|||
|
|||
Most of existing libraries don't support AOT compilation yet, so I couldn't use [BenchmarkDotnet](https://github.com/dotnet/BenchmarkDotNet) to measure the performance. I have used [Stopwatch](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-8.0) to measure the performance. So the performance results may not be accurate but gives insight about the performance difference. |
|||
|
|||
## AOT Support in MAUI |
|||
You can now use Native AOT Compilation on iOS-like target frameworks in .NET MAUI. You can enable AOT compilation with the exact same method by adding `<PublishAot>true</PublishAot>` to your project .csproj file. According to the dotnet team, apps sizes reduced by 35% and startup times reduced by 28% with AOT compilation. And runtime performance is also improved by 50%. |
|||
|
|||
But there are some limitations in MAUI AOT Compilation. A lot of libraries still don't support AOT compilation and some of platform-specific feaetures may not work at the moment. |
|||
|
|||
## When to use Native AOT Compilation? |
|||
|
|||
Native AOT Compilation is beneficial when you need to optimize your .NET application for speed and size. It's particularly useful for applications that require quick startup times and efficient runtime performance, such as mobile apps or high-performance computing applications. |
|||
|
|||
However, due to its current limitations, it might not be suitable for all projects. If your project relies heavily on libraries that do not support AOT compilation, or if it uses platform-specific features that are not yet compatible with AOT, you might want to hold off on using Native AOT Compilation until further improvements are made. |
|||
|
|||
Always consider the specific needs and constraints of your project before deciding to use Native AOT Compilation. |
|||
|
|||
## Conclusion |
|||
|
|||
Native AOT Compilation is a great feature that improves the performance of .NET applications. It's still in early-stages and not all libraries support it yet. But it's a great beginning for the future of .NET 🚀 |
|||
|
|||
|
|||
## Links |
|||
- Native AOT deployment overview - .NET | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/. |
|||
- |
|||
- Optimize AOT deployments https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/optimizing |
|||
- |
|||
- What's new in .NET 8 | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8. |
|||
|
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
public class BundleFile |
|||
{ |
|||
public string FileName { get; set; } |
|||
|
|||
public bool IsExternalFile { get; set; } |
|||
|
|||
public BundleFile(string fileName) |
|||
{ |
|||
FileName = fileName; |
|||
IsExternalFile = fileName.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || |
|||
fileName.StartsWith("https://", StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
public BundleFile(string fileName, bool isExternalFile) |
|||
{ |
|||
FileName = fileName; |
|||
IsExternalFile = isExternalFile; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This method is used to compatible with old code.
|
|||
/// </summary>
|
|||
public static implicit operator BundleFile(string fileName) |
|||
{ |
|||
return new BundleFile(fileName); |
|||
} |
|||
} |
|||
@ -1,22 +1,30 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
public class BundleFileContributor : BundleContributor |
|||
{ |
|||
public string[] Files { get; } |
|||
public List<BundleFile> Files { get; } |
|||
|
|||
public BundleFileContributor(params BundleFile[] files) |
|||
{ |
|||
Files = new List<BundleFile>(); |
|||
Files.AddRange(files); |
|||
} |
|||
|
|||
public BundleFileContributor(params string[] files) |
|||
{ |
|||
Files = files ?? Array.Empty<string>(); |
|||
Files = new List<BundleFile>(); |
|||
Files.AddRange(files.Select(file => new BundleFile(file))); |
|||
} |
|||
|
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
foreach (var file in Files) |
|||
{ |
|||
context.Files.AddIfNotContains(x => x.Equals(file, StringComparison.OrdinalIgnoreCase), () => file); |
|||
context.Files.AddIfNotContains(x => x.FileName.Equals(file.FileName, StringComparison.OrdinalIgnoreCase), () => file); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,8 +1,9 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Resources; |
|||
|
|||
public interface IWebRequestResources |
|||
{ |
|||
List<string> TryAdd(List<string> resources); |
|||
List<BundleFile> TryAdd(List<BundleFile> resources); |
|||
} |
|||
|
|||
@ -0,0 +1,60 @@ |
|||
.radar-spinner, .radar-spinner * { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.radar-spinner { |
|||
height: 60px; |
|||
width: 60px; |
|||
position: relative; |
|||
} |
|||
|
|||
.radar-spinner .circle { |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 100%; |
|||
top: 0; |
|||
left: 0; |
|||
animation: radar-spinner-animation 2s infinite; |
|||
} |
|||
|
|||
.radar-spinner .circle:nth-child(1) { |
|||
padding: calc(60px * 5 * 2 * 0 / 110); |
|||
animation-delay: 300ms; |
|||
} |
|||
|
|||
.radar-spinner .circle:nth-child(2) { |
|||
padding: calc(60px * 5 * 2 * 1 / 110); |
|||
animation-delay: 300ms; |
|||
} |
|||
|
|||
.radar-spinner .circle:nth-child(3) { |
|||
padding: calc(60px * 5 * 2 * 2 / 110); |
|||
animation-delay: 300ms; |
|||
} |
|||
|
|||
.radar-spinner .circle:nth-child(4) { |
|||
padding: calc(60px * 5 * 2 * 3 / 110); |
|||
animation-delay: 0ms; |
|||
} |
|||
|
|||
.radar-spinner .circle-inner, .radar-spinner .circle-inner-container { |
|||
height: 100%; |
|||
width: 100%; |
|||
border-radius: 50%; |
|||
border: calc(60px * 5 / 110) solid transparent; |
|||
} |
|||
|
|||
.radar-spinner .circle-inner { |
|||
border-left-color: var(--secondary, #ff1d5e); |
|||
border-right-color: var(--secondary, #ff1d5e); |
|||
} |
|||
|
|||
@keyframes radar-spinner-animation { |
|||
50% { |
|||
transform: rotate(180deg); |
|||
} |
|||
|
|||
100% { |
|||
transform: rotate(0deg); |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace Volo.Abp.Auditing.App.Entities; |
|||
|
|||
[Audited] |
|||
public class AppEntityWithNavigations : AggregateRoot<Guid> |
|||
{ |
|||
protected AppEntityWithNavigations() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public AppEntityWithNavigations(Guid id, string name) |
|||
: base(id) |
|||
{ |
|||
Name = name; |
|||
FullName = name; |
|||
} |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
public string FullName { get; set; } |
|||
|
|||
public virtual AppEntityWithNavigationChildOneToOne OneToOne { get; set; } |
|||
|
|||
public virtual List<AppEntityWithNavigationChildOneToMany> OneToMany { get; set; } |
|||
|
|||
public virtual List<AppEntityWithNavigationChildManyToMany> ManyToMany { get; set; } |
|||
} |
|||
|
|||
[Audited] |
|||
public class AppEntityWithNavigationChildOneToOne : Entity<Guid> |
|||
{ |
|||
public string ChildName { get; set; } |
|||
} |
|||
|
|||
[Audited] |
|||
public class AppEntityWithNavigationChildOneToMany : Entity<Guid> |
|||
{ |
|||
public Guid AppEntityWithNavigationId { get; set; } |
|||
|
|||
public string ChildName { get; set; } |
|||
} |
|||
|
|||
[Audited] |
|||
public class AppEntityWithNavigationChildManyToMany : Entity<Guid> |
|||
{ |
|||
public string ChildName { get; set; } |
|||
} |
|||
@ -1 +1,3 @@ |
|||
export {}; |
|||
export * from './enums'; |
|||
export * from './models'; |
|||
export * from './constants'; |
|||
|
|||