mirror of https://github.com/abpframework/abp.git
83 changed files with 1450 additions and 229 deletions
@ -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,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'; |
|||
|
|||
Loading…
Reference in new issue