diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 626719b2fc..6df738cf18 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -32,6 +32,8 @@ namespace Acme.BookStore > `ISoftDelete` filter is enabled by default and you can not get deleted entities from database unless you explicitly disable it. See the `IDataFilter` service below. +> Soft-delete entities can be hard-deleted when you use `HardDeleteAsync` method on the repositories. + ### IMultiTenant [Multi-tenancy](Multi-Tenancy.md) is an efficient way of creating SaaS applications. Once you create a multi-tenant application, you typically want to isolate data between tenants. Implement `IMultiTenant` interface to make your entity "multi-tenant aware". diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index e536cfe5cb..8c0a1c9cc9 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -174,7 +174,7 @@ public class Book : AggregateRoot } ``` -(`BookType` is a simple enum here) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): +(`BookType` is a simple `enum` here and not important) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): ````csharp public class BookManager : DomainService @@ -221,12 +221,13 @@ public interface IBookRepository : IRepository } ```` -You generally want to derive from the `IRepository` to inherit standard repository methods. However, you don't have to. Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`EntityFrameworkCore` project in a [startup template](https://abp.io/Templates)). +You generally want to derive from the `IRepository` to inherit standard repository methods (while, you don't have to do). Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`EntityFrameworkCore` project in a [startup template](https://abp.io/Templates)). Example implementation of the `IBookRepository` interface: ````csharp -public class BookRepository : EfCoreRepository, IBookRepository +public class BookRepository + : EfCoreRepository, IBookRepository { public BookRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) @@ -272,6 +273,278 @@ public async override Task DeleteAsync( } ```` +## Loading Related Entities + +Assume that you've an `Order` with a collection of `OrderLine`s and the `OrderLine` has a navigation property to the `Order`: + +````csharp +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; + +namespace MyCrm +{ + public class Order : AggregateRoot, IHasCreationTime + { + public Guid CustomerId { get; set; } + public DateTime CreationTime { get; set; } + + public ICollection Lines { get; set; } //Sub collection + + public Order() + { + Lines = new Collection(); + } + } + + public class OrderLine : Entity + { + public Order Order { get; set; } //Navigation property + public Guid OrderId { get; set; } + + public Guid ProductId { get; set; } + public int Count { get; set; } + public double UnitPrice { get; set; } + } +} + +```` + +And defined the database mapping as shown below: + +````csharp +builder.Entity(b => +{ + b.ToTable("Orders"); + b.ConfigureByConvention(); + + //Define the relation + b.HasMany(x => x.Lines) + .WithOne(x => x.Order) + .HasForeignKey(x => x.OrderId) + .IsRequired(); +}); + +builder.Entity(b => +{ + b.ToTable("OrderLines"); + b.ConfigureByConvention(); +}); +```` + +When you query an `Order`, you may want to **include** all the `OrderLine`s in a single query or you may want to **load them later** on demand. + +> Actually these are not directly related to the ABP Framework. You can follow the [EF Core documentation](https://docs.microsoft.com/en-us/ef/core/querying/related-data/) to learn all the details. This section will cover some topics related to the ABP Framework. + +### Eager Loading / Load With Details + +You have different options when you want to load the related entities while querying an entity. + +#### Repository.WithDetails + +`IRepository.WithDetails(...)` can be used to include one relation collection/property to the query. + +**Example: Get an order with lines** + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace MyCrm +{ + public class OrderManager : DomainService + { + private readonly IRepository _orderRepository; + + public OrderManager(IRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task TestWithDetails(Guid id) + { + var query = _orderRepository + .WithDetails(x => x.Lines) + .Where(x => x.Id == id); + + var order = await AsyncExecuter.FirstOrDefaultAsync(query); + } + } +} +```` + +> `AsyncExecuter` is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use `await _orderRepository.WithDetails(x => x.Lines).FirstOrDefaultAsync()`. But, this time you depend on the EF Core in your domain layer. See the [repository document](Repositories.md) to learn more. + +**Example: Get a list of orders with their lines** + +````csharp +public async Task TestWithDetails() +{ + var query = _orderRepository + .WithDetails(x => x.Lines); + + var orders = await AsyncExecuter.ToListAsync(query); +} +```` + +> `WithDetails` method can get more than one expression parameter if you need to include more than one navigation property or collection. + +#### DefaultWithDetailsFunc + +If you don't pass any expression to the `WithDetails` method, then it includes all the details using the `DefaultWithDetailsFunc` option you provide. + +You can configure `DefaultWithDetailsFunc` for an entity in the `ConfigureServices` method of your [module](Module-Development-Basics.md) in your `EntityFrameworkCore` project. + +**Example: Include `Lines` while querying an `Order`** + +````csharp +Configure(options => +{ + options.Entity(orderOptions => + { + orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines); + }); +}); +```` + +> You can fully use the EF Core API here since this is located in the EF Core integration project. + +Then you can use the `WithDetails` without any parameter: + +````csharp +public async Task TestWithDetails() +{ + var query = _orderRepository.WithDetails(); + var orders = await AsyncExecuter.ToListAsync(query); +} +```` + +`WithDetails()` executes the expression you've setup as the `DefaultWithDetailsFunc`. + +#### Repository Get/Find Methods + +Some of the standard [Repository](Repositories.md) methods have optional `includeDetails` parameters; + +* `GetAsync` and `FindAsync` gets `includeDetails` with default value is `true`. +* `GetListAsync` and `GetPagedListAsync` gets `includeDetails` with default value is `false`. + +That means, the methods return a **single entity includes details** by default while list returning methods don't include details by default. You can explicitly pass `includeDetails` to change the behavior. + +> These methods use the `DefaultWithDetailsFunc` option that is explained above. + +**Example: Get an order with details** + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id); +} +```` + +**Example: Get an order without details** + +````csharp +public async Task TestWithoutDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id, includeDetails: false); +} +```` + +**Example: Get list of entities with details** + +````csharp +public async Task TestWithDetails() +{ + var orders = await _orderRepository.GetListAsync(includeDetails: true); +} +```` + +#### Alternatives + +The repository patters tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options; + +* Create a custom repository method and use the complete EF Core API. +* Reference to the `Volo.Abp.EntityFrameworkCore` package from your project. In this way, you can directly use `Include` and `ThenInclude` in your code. + +See also [eager loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/eager) of the EF Core. + +### Explicit / Lazy Loading + +If you don't include relations while querying an entity and later need to access to a navigation property or collection, you have different options. + +#### EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync + +Repositories provide `EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` extension methods to **explicitly load** a navigation property or sub collection. + +**Example: Load Lines of an Order when needed** + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id, includeDetails: false); + //order.Lines is empty on this stage + + await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines); + //order.Lines is filled now +} +```` + +`EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` methods do nothing if the property or collection was already loaded. So, calling multiple times has no problem. + +See also [explicit loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/explicit) of the EF Core. + +#### Lazy Loading with Proxies + +Explicit loading may not be possible in some cases, especially when you don't have a reference to the `Repository` or `DbContext`. Lazy Loading is a feature of the EF Core that loads the related properties / collections when you first access to it. + +To enable lazy loading; + +1. Install the [Microsoft.EntityFrameworkCore.Proxies](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies/) package into your project (typically to the EF Core integration project) +2. Configure `UseLazyLoadingProxies` for your `DbContext` (in the `ConfigureServices` method of your module in your EF Core project). Example: + +````csharp +Configure(options => +{ + options.PreConfigure(opts => + { + opts.DbContextOptions.UseLazyLoadingProxies(); //Enable lazy loading + }); + + options.UseSqlServer(); +}); +```` + +3. Make your navigation properties and collections `virtual`. Examples: + +````csharp +public virtual ICollection Lines { get; set; } //virtual collection +public virtual Order Order { get; set; } //virtual navigation property +```` + +Once you enable lazy loading and arrange your entities, you can freely access to the navigation properties and collections: + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id); + //order.Lines is empty on this stage + + var lines = order.Lines; + //order.Lines is filled (lazy loaded) +} +```` + +Whenever you access to a property/collection, EF Core automatically performs an additional query to load the property/collection from the database. + +> Lazy loading should be carefully used since it may cause performance problems in some specific cases. + +See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) of the EF Core. + ## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: @@ -296,7 +569,7 @@ public class BookService * `GetDbContext` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it, however in most cases you don't need it. -> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case. +> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the `DbContext`. This breaks encapsulation, but this is what you want in that case. ## Extra Properties & Object Extension Manager @@ -446,23 +719,22 @@ context.Services.AddAbpDbContext(options => In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. -### Enabling split queries globally by default +### Split Queries -Abp enables split queries globally by default for better performance. You can also change it as needed, such as: +ABP enables [split queries](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries) globally by default for better performance. You can change it as needed. + +**Example** ````csharp Configure(options => { options.UseSqlServer(optionsBuilder => { - //Change QuerySplittingBehavior optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); }); }); ```` -For more information, please refer to https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.querysplittingbehavior?view=efcore-5.0 - ## See Also * [Entities](Entities.md) diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 0fc80984e7..5ff8372af8 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -79,6 +79,14 @@ If your entity does not have an Id primary key (it may have a composite primary > `IRepository` has a few missing methods those normally works with the `Id` property of an entity. Because of the entity has no `Id` property in that case, these methods are not available. One example is the `Get` method that gets an id and returns the entity with given id. However, you can still use `IQueryable` features to query entities by standard LINQ methods. +### Soft / Hard Delete + +`DeleteAsync` method of the repository doesn't delete the entity if the entity is a **soft-delete** entity (that implements `ISoftDelete`). Soft-delete entities are marked as "deleted" in the database. Data Filter system ensures that the soft deleted entities are not retrieved from database normally. + +If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method to really delete the entity from database in case of you need it. + +See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. + ## Custom Repositories Default generic repositories will be sufficient for most cases. However, you may need to create a custom repository class for your entity. diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts index b16b2bb14c..b1fbef79be 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts @@ -4,8 +4,10 @@ import { ContentChild, ElementRef, EventEmitter, + Inject, Input, OnDestroy, + Optional, Output, Renderer2, TemplateRef, @@ -18,6 +20,7 @@ import { fadeAnimation } from '../../animations/modal.animations'; import { Confirmation } from '../../models/confirmation'; import { ConfirmationService } from '../../services/confirmation.service'; import { ModalService } from '../../services/modal.service'; +import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '../../tokens/suppress-unsaved-changes-warning.token'; import { ButtonComponent } from '../button/button.component'; export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'; @@ -57,6 +60,8 @@ export class ModalComponent implements OnDestroy { @Input() size: ModalSize = 'lg'; + @Input() suppressUnsavedChangesWarning = this.suppressUnsavedChangesWarningToken; + @ContentChild(ButtonComponent, { static: false, read: ButtonComponent }) abpSubmit: ButtonComponent; @@ -104,6 +109,9 @@ export class ModalComponent implements OnDestroy { private confirmationService: ConfirmationService, private modalService: ModalService, private subscription: SubscriptionService, + @Optional() + @Inject(SUPPRESS_UNSAVED_CHANGES_WARNING) + private suppressUnsavedChangesWarningToken: boolean, ) { this.initToggleStream(); } @@ -139,7 +147,7 @@ export class ModalComponent implements OnDestroy { close() { if (this.busy) return; - if (this.isFormDirty) { + if (this.isFormDirty && !this.suppressUnsavedChangesWarning) { if (this.isConfirmationOpen) return; this.isConfirmationOpen = true; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tokens/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/tokens/index.ts index a923c8fc23..18a2aac2f6 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tokens/index.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tokens/index.ts @@ -1,3 +1,4 @@ export * from './append-content.token'; export * from './http-error.token'; export * from './lazy-styles.token'; +export * from './suppress-unsaved-changes-warning.token'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts b/npm/ng-packs/packages/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts new file mode 100644 index 0000000000..af68c8130c --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; + +// TODO: Should be documented +export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken( + 'SUPPRESS_UNSAVED_CHANGES_WARNING', +);