Merge into cms-kit/blog-initial from dev conflict fix

pull/7596/head
enisn 5 years ago
commit a362c077be

@ -199,9 +199,20 @@
"Optional": "Optional",
"CreateArticleLanguageInfo": "The language in which the post is written",
"Enum:ContentSource:2": "Video Post",
"VideoPreview": "Video Preview",
"VideoPreviewErrorMessage": "Given video url couldn't retrieve from Youtube. This can be caused by either video is private or the given URL is not available.",
"DeleteCoverImage": "Delete Cover Image",
"DeleteCoverImageConfirmationMessage": "Are you sure you want to delete the cover image for \"{0}\"?",
"DeleteCoverImageSuccessMessage": "Cover image successfully deleted"
"DeleteCoverImageSuccessMessage": "Cover image successfully deleted",
"PaymentsOf": "Payments of",
"ShowPaymentsOfOrganization": "Show payments",
"Date": "Date",
"Products": "Products",
"TotalAmount": "Total amount",
"Currency": "Currency",
"Gateway": "Gateway",
"State": "State",
"FailReason": "Fail reason"
}
}

@ -132,6 +132,10 @@
"ChooseYourContentType": "Please choose the way you want to add your content.",
"PostContentViaGithub": "I want to add my article with <span class=\"font-weight-bold\"><i class=\"fa fa-github\"></i> GitHub</span> in accordance with the markdown rules.",
"PostContentViaYoutube": "I want to share my videos available on <span class=\"font-weight-bold\"><i class=\"fa fa-youtube\"></i> Youtube</span> here.",
"PostContentViaExternalSource": "I want to add the content I published on <span class=\"font-weight-bold\">another platform</span> here."
"PostContentViaExternalSource": "I want to add the content I published on <span class=\"font-weight-bold\">another platform</span> here.",
"GitHubUserNameValidationMessage": "Your Github username can not include whitespace, please be sure your Github username is correct.",
"PersonalSiteUrlValidationMessage": "Your personal site URL can not include whitespace, please be sure your personal site URL is correct.",
"TwitterUserNameValidationMessage": "Your Twitter username can not include whitespace, please be sure your Twitter username is correct.",
"LinkedinUrlValidationMessage": "Your Linkedin URL can not include whitespace, please be sure your Linkedin URL is correct."
}
}

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>4.3.0</Version>
<LangVersion>latest</LangVersion>
<Version>4.3.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

@ -43,12 +43,14 @@ public class Book : AggregateRoot<Guid>
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException($"name can not be empty or white space!");
throw new ArgumentException(
$"name can not be empty or white space!");
}
if (name.Length > MaxNameLength)
{
throw new ArgumentException($"name can not be longer than {MaxNameLength} chars!");
throw new ArgumentException(
$"name can not be longer than {MaxNameLength} chars!");
}
return name;
@ -349,8 +351,9 @@ public class DistrictAppService
protected async override Task<District> GetEntityByIdAsync(DistrictKey id)
{
var queryable = await Repository.GetQueryableAsync();
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
queryable.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}

@ -184,7 +184,7 @@ public class PersonAppService : ApplicationService, IPersonAppService
}
````
ABP Framework's repository doesn't have this method. Instead, it implements the `IQueryable` itself. So, you can directly use LINQ on the repository:
ABP Framework's repository have `GetQueryableAsync` instead:
````csharp
public class PersonAppService : ApplicationService, IPersonAppService
@ -198,14 +198,15 @@ public class PersonAppService : ApplicationService, IPersonAppService
public async Task DoIt()
{
var people = await _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = await queryable
.Where(p => p.BirthYear > 2000) //Use LINQ extension methods
.ToListAsync();
}
}
````
> Note that in order to use the async LINQ extension methods (like `ToListAsync` here), you may need to depend on the database provider (like EF Core) since these methods are defined in the database provider package, they are not standard LINQ methods.
> Note that in order to use the async LINQ extension methods (like `ToListAsync` here), you may need to depend on the database provider (like EF Core) since these methods are defined in the database provider package, they are not standard LINQ methods. See the [repository document](Repositories.md) for alternative approaches for async query execution.
#### FirstOrDefault(predicate), Single()... Methods

@ -144,7 +144,7 @@ public virtual async Task<IdentityUser> FindByNormalizedUserNameAsync(
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await DbSet
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.FirstOrDefaultAsync(
u => u.NormalizedUserName == normalizedUserName,
@ -175,14 +175,15 @@ public static IQueryable<IdentityUser> IncludeDetails(
}
````
* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see FindByNormalizedUserNameAsync).
* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see `FindByNormalizedUserNameAsync`).
- **Do** override `WithDetails` method of the repository for aggregates root which have **sub collections**. Example:
````C#
public override IQueryable<IdentityUser> WithDetails()
public override async Task<IQueryable<IdentityUser>> WithDetailsAsync()
{
return GetQueryable().IncludeDetails(); // Uses the extension method defined above
// Uses the extension method defined above
return (await GetQueryableAsync()).IncludeDetails();
}
````

@ -128,7 +128,7 @@ public async Task<IdentityUser> FindByNormalizedUserNameAsync(
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable()
return await (await GetMongoQueryableAsync())
.FirstOrDefaultAsync(
u => u.NormalizedUserName == normalizedUserName,
GetCancellationToken(cancellationToken)
@ -139,8 +139,8 @@ public async Task<IdentityUser> FindByNormalizedUserNameAsync(
`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code.
* **Do** ignore the `includeDetails` parameters for the repository implementation since MongoDB loads the aggregate root as a whole (including sub collections) by default.
* **Do** use the `GetMongoQueryable()` method to obtain an `IQueryable<TEntity>` to perform queries wherever possible. Because;
* `GetMongoQueryable()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy).
* **Do** use the `GetMongoQueryableAsync()` method to obtain an `IQueryable<TEntity>` to perform queries wherever possible. Because;
* `GetMongoQueryableAsync()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy).
* Using `IQueryable<TEntity>` makes the code as much as similar to the EF Core repository implementation and easy to write and read.
* **Do** implement data filtering if it is not possible to use the `GetMongoQueryable()` method.

@ -42,24 +42,6 @@ Task<IdentityUser> FindByNormalizedUserNameAsync(
);
````
* **Do** create a **synchronous extension** method for each asynchronous repository method. Example:
````C#
public static class IdentityUserRepositoryExtensions
{
public static IdentityUser FindByNormalizedUserName(
this IIdentityUserRepository repository,
[NotNull] string normalizedUserName)
{
return AsyncHelper.RunSync(
() => repository.FindByNormalizedUserNameAsync(normalizedUserName)
);
}
}
````
This will allow synchronous code to use the repository methods easier.
* **Do** add an optional `bool includeDetails = true` parameter (default value is `true`) for every repository method which returns a **single entity**. Example:
````C#

@ -0,0 +1,53 @@
# ABP.IO Platform 4.2 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.2 versions have been released today.
## What's New With 4.2?
Since all the new features are already explained in details with the [4.2 RC Announcement Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP-IO-Platform-v4-2-RC-Has-Been-Released) for all the features and enhancements.
## Creating New Solutions
You can create a new solution with the ABP Framework version 4.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started).
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details.
## How to Upgrade an Existing Solution
### Install/Update the ABP CLI
First of all, install the ABP CLI or upgrade to the latest version.
If you haven't installed yet:
```bash
dotnet tool install -g Volo.Abp.Cli
```
To update an existing installation:
```bash
dotnet tool update -g Volo.Abp.Cli
```
### ABP UPDATE Command
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command:
```bash
abp update
```
Run this command in the root folder of your solution.
## Migration Guide
Check [the migration guide](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_2) for the applications with the version 4.x upgrading to the version 4.2.
> It is strongly recommended to check the migration guide for this version. Especially, the new `IRepository.GetQueryableAsync()` method is a core change should be considered after upgrading the solution.
## About the Next Version
The next feature version will be 4.3. It is planned to release the 4.3 RC (Release Candidate) on March 11 and the final version on March 25, 2021.
We decided to slow down the feature development for the [next milestone](https://github.com/abpframework/abp/milestone/49). We will continue to improve the existing features and introduce new ones, sure, but wanted to have more time for the planning, documentation, creating guides and improving the development experience.

@ -0,0 +1,103 @@
## Using MatBlazor UI Components With the ABP Framework
Hi, in this step by step article, I will show you how to integrate [MatBlazor](https://www.matblazor.com/), a blazor UI components into ABP Framework-based applications.
![example-result](example-result.png)
*(A screenshot from the example application developed in this article)*
## Create the Project
> First thing is to create the project. ABP Framework offers startup templates to get into business faster.
In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project.
> If you already have a project with the Blazor UI, you can skip this section.
* Before starting the development, we will create a new solution named `MatBlazorSample` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI):
````bash
abp new MatBlazorSample -u blazor
````
This will create new project inside of `aspnet-core`, so:
````bash
cd aspnet-core
````
and
````bash
dotnet restore
````
* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `MatBlazorSample.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions etc.)
![initial-project](initial-project.png)
* After database and initial data created,
* Run the `MatBlazorSample.HttpApi.Host` to see our server side working and
* Run the `MatBlazorSample.Blazor` to see our UI working properly.
> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_
## Install MatBlazor
You can follow [this documentation](https://www.matblazor.com/) to install MatBlazor packages into your computer.
### Adding MatBlazor NuGet Packages
```bash
Install-Package MatBlazor
```
### Register MatBlazor Resources
1. Add the following line to the HEAD section of the `wwwroot/index.html` file within the `MatBlazorSample.Blazor` project:
```Razor
<head>
<!--...-->
<script src="_content/MatBlazor/dist/matBlazor.js"></script>
<link href="_content/MatBlazor/dist/matBlazor.css" rel="stylesheet" />
</head>
```
2. In the `MatBlazorSampleBlazorModule` class, call the `AddMatBlazor()` method from your project's `ConfigureServices()` method:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var environment = context.Services.GetSingletonInstance<IWebAssemblyHostEnvironment>();
var builder = context.Services.GetSingletonInstance<WebAssemblyHostBuilder>();
// ...
builder.Services.AddMatBlazor();
}
```
3. Register the **MatBlazorSample.Blazor** namespace in the `_Imports.razor` file:
```Razor
@using MatBlazor
```
## The Sample Application
We have created a sample application with [Table](https://www.matblazor.com/Table) example.
### The Source Code
You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/MatBlazorSample).
The related files for this example are marked in the following screenshots.
![table-app-contract](table-app-contract.png)
![table-application](table-application.png)
![table-web](table-web.png)
## Conclusion
In this article, I've explained how to use [MatBlazor](https://www.matblazor.com/) components in your application. ABP Framework is designed so that it can work with any UI library/framework.

@ -0,0 +1,343 @@
# How to Use PrimeNG Components with the ABP Angular UI
## Introduction
In this article, we will use components of the [PrimeNG](https://www.primefaces.org/primeng/) that is a popular UI component library for Angular with the ABP Framework Angular UI that will be generated via [ABP CLI](https://docs.abp.io/en/abp/latest/CLI).
We will create an organization units page and use PrimeNG's [OrganizationChart](https://primefaces.org/primeng/showcase/#/organizationchart) and [Table](https://primefaces.org/primeng/showcase/#/table) components on the page.
![Introduction](intro.gif)
<small>The UI shown above contains many PrimeNG components. You can reach the source code of this rich UI. Take a look at the source code section below.</small>
> This article does not cover any backend code. I used mock data to provide data source to the components.
## Pre-Requirements
The following tools should be installed on your development machine:
* [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12 or v14](https://nodejs.org/)
* [VS Code](https://code.visualstudio.com/) or another IDE
## Source Code
I have prepared a sample project that contains more PrimeNG components than described in this article. You can download the source code [on GitHub](https://github.com/abpframework/abp-samples/tree/master/PrimengSample).
## Creating a New Solution
In this step, we will create a new solution that contains Angular UI and backend startup templates. If you have a startup template with Angular UI, you can skip this step.
Run the following command to install the ABP CLI:
```bash
dotnet tool install -g Volo.Abp.Cli
```
...or update:
```bash
dotnet tool update -g Volo.Abp.Cli
```
Create a new solution named `AbpPrimengSample` by running the following command:
```bash
abp new AbpPrimengSample -u angular -csf
```
See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all available options.
You can also use the Direct Download tab on the [Get Started](https://abp.io/get-started) page.
## Running the Solution
You can run the solution as described in [here](https://docs.abp.io/en/abp/latest/Getting-Started-Running-Solution?UI=NG&DB=EF&Tiered=No).
## PrimeNG Setup
Open the `angular` folder and run the following command to install packages:
```bash
npm install
```
Next, we need to install `primeng` and required packages (`primeicons` and `@angular/cdk`) for the library. Run the command below to install these packages:
```bash
npm install primeng primeicons @angular/cdk --save
```
The packages we have installed;
- `primeng` is the main package that is a component library.
- `primeicons` is an icon font library. Many PrimeNG components use this font internally.
- `@angular/cdk` is a component dev kit created by the Angular team. Some PrimeNG modules depend on it.
As the last step of the setup, we should add the required style files for the library to `angular.json`:
```js
//angular.json
"projects": {
"AbpPrimengSample": {
//...
"styles": {
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/themes/saga-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
//...other styles
}
}
}
```
We have added the `primeng.min.css`, Saga Blue theme's `theme.css`, and `primeicons.css` files to the project. You can choose another theme instead of the Sage Blue. See available themes on the [Get Started](https://www.primefaces.org/primeng/showcase/#/setup) document of the PrimeNG.
> You have to restart the running `ng serve` process to see the effect of the changes you made in the `angular.json`.
## Creating the Organization Units Page
Run the following command to create a new module named `OrganizationUnits`:
```bash
npm run ng -- generate module organization-units --route organization-units --module app.module
```
Then open the `src/route.provider.ts` and add a new route as an array element to add a navigation link labeled "Organization Units" to the menu:
```js
//route.provider.ts
import { eThemeSharedRouteNames } from '@abp/ng.theme.shared';
//...
routesService.add([
//...
{
path: '/organization-units',
name: 'Organization Units',
parentName: eThemeSharedRouteNames.Administration,
iconClass: 'fas fa-sitemap',
layout: eLayoutType.application,
},
]);
```
We have created a lazy-loadable module and defined a menu navigation link. We can navigate to the page as shown below:
![organization units menu navigation item](organization-units-menu-item.jpg)
## Using the PrimeNG Components
### Implementing the Organization Chart Component
When you would like to use any component from PrimeNG, you have to import the component's module to your module. Since we will use the `OrganizationChart` on the organization units page, we need to import `OrganizationChartModule` into `OrganizationUnitsModule`.
Open the `src/organization-units/organization-units.module.ts` and add the `OrganizationChartModule` to the imports array as shown below:
```js
import { OrganizationChartModule } from 'primeng/organizationchart';
//...
@NgModule({
//...
imports: [
//...
OrganizationChartModule
],
})
export class OrganizationUnitsModule {}
```
> Since NGCC need to work in some cases, restarting the `ng serve` process would be good when you import any modules from `primeng` package to your module.
Let's define a mock data source for the `OrganizationChartComponent` and add the component to the page.
Open the `src/organization-units/organization-units.component.ts` and add two variables as shown below:
```js
//...
import { TreeNode } from 'primeng/api';
@Component(/* component metadata*/)
export class OrganizationUnitsComponent implements OnInit {
//...
organizationUnits: TreeNode[] = [
{
label: 'Management',
expanded: true,
children: [
{
label: 'Selling',
expanded: true,
children: [
{
label: 'Customer Relations',
},
{
label: 'Marketing',
},
],
},
{
label: 'Supporting',
expanded: true,
children: [
{
label: 'Buying',
},
{
label: 'Human Resources',
},
],
},
],
},
];
selectedUnit: TreeNode;
```
- First variable is `organizationUnits`. It provides mock data source to `OrganizationChartComponent`.
- Second variable is `selectedUnit`. It keeps chosen unit on the chart.
Then, open the `src/organization-units/organization-units.component.html` and replace the file content with the following:
```html
<div class="card">
<div class="card-header">
<h5>Organization Units</h5>
</div>
<div class="card-body">
<p-organizationChart
[value]="organizationUnits"
selectionMode="single"
[(selection)]="selectedUnit"
></p-organizationChart>
</div>
</div>
```
We have implemented the `OrganizationChart`. The final UI looks like below:
![organization chart](organization-chart.jpg)
## Implementing the Table Component
In order to use the `TableComponent`, we have to import the `TableModule` to the `OrganizationUnitsModule`.
Open the `organization-units.module.ts` and add `TableModule` to the imports array as shown below:
```js
import { TableModule } from 'primeng/table';
//...
@NgModule({
//...
imports: [
//...
TableModule
],
})
export class OrganizationUnitsModule {}
```
Open the `organization-units.component.ts` and add a variable named `members` with initial value and add a getter named `tableData` as shown below:
```js
//...
export class OrganizationUnitsComponent implements OnInit {
//...
members = [
{
fullName: 'John Doe',
username: 'John.Doe',
phone: '+1-202-555-0125',
email: 'john.doe@example.com',
parent: 'Customer Relations',
},
{
fullName: 'Darrion Walter',
username: 'Darrion.Walter',
phone: '+1-262-155-0355',
email: 'Darrion_Walter@example.com',
parent: 'Marketing',
},
{
fullName: 'Rosa Labadie',
username: 'Rosa.Labadie',
phone: '+1-262-723-2255',
email: 'Rosa.Labadie@example.com',
parent: 'Marketing',
},
{
fullName: 'Adelle Hills',
username: 'Adelle.Hills',
phone: '+1-491-112-9011',
email: 'Adelle.Hills@example.com',
parent: 'Buying',
},
{
fullName: 'Brian Hane',
username: 'Brian.Hane',
phone: '+1-772-509-1823',
email: 'Brian.Hane@example.com',
parent: 'Human Resources',
},
];
get tableData() {
return this.members.filter(user => user.parent === this.selectedUnit.label);
}
```
What we have done above?
- We defined a variable named `members` to provide mock data to the table.
- We have defined a getter named `tableData` to provide filtered data source to the table using `members` variable.
We are now ready to add the table to the HTML template.
Open the `organization-units.component.html`, find the `p-organizationChart` tag and place the following code to the bottom of this tag:
```html
<div class="p-3" *ngIf="selectedUnit">
<h6>Members of {{ selectedUnit.label }}</h6>
<p-table [value]="tableData">
<ng-template pTemplate="header">
<tr>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>Phone</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-member>
<tr>
<td>{{ member.fullName }}</td>
<td>{{ member.username }}</td>
<td>{{ member.email }}</td>
<td>{{ member.phone }}</td>
</tr>
</ng-template>
</p-table>
</div>
```
We have added a new `div` that contains the `TableComponent`. The table appears when an organization unit is selected.
The table contains 4 columns which are name, username, email, and phone for displaying the members' information.
After adding the table, the final UI looks like this:
![PrimeNG TableComponent](table.gif)
## Conclusion
We have implemented the PrimeNG component library on the ABP Angular UI project and used two components on a page in a short time. You can use any PrimeNG components by following the documentation. The ABP Angular UI will not block you in any case.

@ -1,6 +1,6 @@
# Dependency Injection
ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too.
ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too.
> While ABP has no core dependency to any 3rd-party DI provider, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with Autofac installed. See [Autofac integration](Autofac-Integration.md) document for more information.

@ -754,7 +754,8 @@ namespace IssueTracking.Issues
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return await DbSet.Where(i =>
var dbSet = await GetDbSetAsync();
return await dbSet.Where(i =>
//Open
!i.IsClosed &&
@ -906,7 +907,8 @@ public class EfCoreIssueRepository :
public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
return await DbSet
var dbSet = await GetDbSetAsync();
return await dbSet
.Where(spec.ToExpression())
.ToListAsync();
}
@ -952,8 +954,9 @@ public class IssueAppService : ApplicationService, IIssueAppService
public async Task DoItAsync()
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
_issueRepository.Where(new InActiveIssueSpecification())
queryable.Where(new InActiveIssueSpecification())
);
}
}
@ -996,8 +999,9 @@ public class IssueAppService : ApplicationService, IIssueAppService
public async Task DoItAsync(Guid milestoneId)
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
_issueRepository
queryable
.Where(
new InActiveIssueSpecification()
.And(new MilestoneSpecification(milestoneId))

@ -236,7 +236,8 @@ public class BookRepository
public async Task DeleteBooksByType(BookType type)
{
await DbContext.Database.ExecuteSqlRawAsync(
var dbContext = await GetDbContextAsync();
await dbContext.Database.ExecuteSqlRawAsync(
$"DELETE FROM Books WHERE Type = {(int)type}"
);
}
@ -344,7 +345,7 @@ You have different options when you want to load the related entities while quer
#### Repository.WithDetails
`IRepository.WithDetails(...)` can be used to include one relation collection/property to the query.
`IRepository.WithDetailsAsync(...)` can be used to get an `IQueryable<T>` by including one relation collection/property.
**Example: Get an order with lines**
@ -355,7 +356,7 @@ using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
namespace MyCrm
namespace AbpDemo.Orders
{
public class OrderManager : DomainService
{
@ -368,35 +369,39 @@ namespace MyCrm
public async Task TestWithDetails(Guid id)
{
var query = _orderRepository
.WithDetails(x => x.Lines)
.Where(x => x.Id == id);
//Get a IQueryable<T> by including sub collections
var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
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.
> `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 query.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);
//Get a IQueryable<T> by including sub collections
var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);
var orders = await AsyncExecuter.ToListAsync(query);
//Execute the query and get the result
var orders = await AsyncExecuter.ToListAsync(queryable);
}
````
> `WithDetails` method can get more than one expression parameter if you need to include more than one navigation property or collection.
> `WithDetailsAsync` 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.
If you don't pass any expression to the `WithDetailsAsync` 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.
@ -419,12 +424,15 @@ Then you can use the `WithDetails` without any parameter:
````csharp
public async Task TestWithDetails()
{
var query = _orderRepository.WithDetails();
var orders = await AsyncExecuter.ToListAsync(query);
//Get a IQueryable<T> by including all sub collections
var queryable = await _orderRepository.WithDetailsAsync();
//Execute the query and get the result
var orders = await AsyncExecuter.ToListAsync(queryable);
}
````
`WithDetails()` executes the expression you've setup as the `DefaultWithDetailsFunc`.
`WithDetailsAsync()` executes the expression you've setup as the `DefaultWithDetailsFunc`.
#### Repository Get/Find Methods
@ -466,7 +474,7 @@ public async Task TestWithDetails()
#### 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;
The repository pattern 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.
@ -550,24 +558,15 @@ See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/queryi
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:
````csharp
public class BookService
public async Task TestAsync()
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public void Foo()
{
DbContext dbContext = _bookRepository.GetDbContext();
DbSet<Book> books = _bookRepository.GetDbSet();
}
var dbContext = await _orderRepository.GetDbContextAsync();
var dbSet = await _orderRepository.GetDbSetAsync();
//var dbSet = dbContext.Set<Order>(); //Alternative, when you have the DbContext
}
````
* `GetDbContext` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it, however in most cases you don't need it.
* `GetDbContextAsync` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it if you need. However, you don't need it in most cases.
> 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.
@ -742,32 +741,36 @@ If you have better logic or using an external library for bulk operations, you c
- You may use example template below:
```csharp
public class MyCustomEfCoreBulkOperationProvider : IEfCoreBulkOperationProvider, ITransientDependency
{
public async Task DeleteManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
public class MyCustomEfCoreBulkOperationProvider
: IEfCoreBulkOperationProvider, ITransientDependency
{
public async Task DeleteManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task InsertManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
public async Task InsertManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task UpdateManyAsync<TDbContext, TEntity>(IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
public async Task UpdateManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
@ -779,3 +782,4 @@ public class MyCustomEfCoreBulkOperationProvider : IEfCoreBulkOperationProvider,
## See Also
* [Entities](Entities.md)
* [Repositories](Repositories.md)

@ -0,0 +1,101 @@
# ABP version 4.2 Migration Guide
This version has no breaking changes but there is an important change on the repositories that should be applied for your application for an important performance and scalability gain.
## IRepository.GetQueryableAsync
`IRepository` interface inherits `IQueryable`, so you can directly use the standard LINQ extension methods, like `Where`, `OrderBy`, `First`, `Sum`... etc.
**Example: Using LINQ directly over the repository object**
````csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookAppService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task DoItInOldWayAsync()
{
//Apply any standard LINQ extension method
var query = _bookRepository
.Where(x => x.Price > 10)
.OrderBy(x => x.Name);
//Execute the query asynchronously
var books = await AsyncExecuter.ToListAsync(query);
}
}
````
*See [the documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) if you wonder what is the `AsyncExecuter`.*
**Beginning from the version 4.2, the recommended way is using `IRepository.GetQueryableAsync()` to obtain an `IQueryable`, then use the LINQ extension methods over it.**
**Example: Using the new GetQueryableAsync method**
````csharp
public async Task DoItInNewWayAsync()
{
//Use GetQueryableAsync to obtain the IQueryable<Book> first
var queryable = await _bookRepository.GetQueryableAsync();
//Then apply any standard LINQ extension method
var query = queryable
.Where(x => x.Price > 10)
.OrderBy(x => x.Name);
//Finally, execute the query asynchronously
var books = await AsyncExecuter.ToListAsync(query);
}
````
ABP may start a database transaction when you get an `IQueryable` (If current [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) is transactional). In this new way, it is possible to **start the database transaction in an asynchronous way**. Previously, we could not get the advantage of asynchronous while starting the transactions.
> **The new way has a significant performance and scalability gain. The old usage (directly using LINQ over the repositories) will be removed in the next major version (5.0).** You have a lot of time for the change, but we recommend to immediately take the action since the old usage has a big **scalability problem**.
### Actions to Take
* Use the repository's queryable feature as explained before.
* If you've overridden `CreateFilteredQuery` in a class derived from `CrudAppService`, you should override the `CreateFilteredQueryAsync` instead and remove the `CreateFilteredQuery` in your class.
* If you've overridden `WithDetails` in your custom repositories, remove it and override `WithDetailsAsync` instead.
* If you've used `DbContext` or `DbSet` properties in your custom repositories, use `GetDbContextAsync()` and `GetDbSetAsync()` methods instead of them.
You can re-build your solution and check the `Obsolete` warnings to find some of the usages need to change.
#### About IRepository Async Extension Methods
Using IRepository Async Extension Methods has no such a problem. The examples below are pretty fine:
````csharp
var countAll = await _personRepository
.CountAsync();
var count = await _personRepository
.CountAsync(x => x.Name.StartsWith("A"));
var book1984 = await _bookRepository
.FirstOrDefaultAsync(x => x.Name == "John");
````
See the [repository documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) to understand the relation between `IQueryable` and asynchronous operations.
## .NET Package Upgrades
ABP uses the latest 5.0.* .NET packages. If your application is using 5.0.0 packages, you may get an error on build. We recommend to depend on the .NET packages like `5.0.*` in the `.csproj` files to use the latest patch versions.
Example:
````xml
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.*" />
````
## Blazorise Library Upgrade
If you are upgrading to 4.2, you also need also upgrade the following packages in your Blazor application;
* `Blazorise.Bootstrap` to `0.9.3-preview6`
* `Blazorise.Icons.FontAwesome` to `0.9.3-preview6`

@ -1,5 +1,6 @@
# ABP Framework Migration Guides
* [3.3.x to 4.0 Migration Guide](Abp-4_0.md)
* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md)
* [4.x to 4.2](Abp-4_2.md)
* [3.3.x to 4.0](Abp-4_0.md)
* [2.9.x to 3.0](../UI/Angular/Migration-Guide-v3.md)

@ -149,7 +149,7 @@ public class Book : AggregateRoot<Guid>
}
```
(`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 you want to create a new `Book` entity in a [domain service](Domain-Services.md):
```csharp
public class BookManager : DomainService
@ -215,7 +215,8 @@ public class BookRepository :
BookType type,
CancellationToken cancellationToken = default(CancellationToken))
{
await Collection.DeleteManyAsync(
var collection = await GetCollectionAsync(cancellationToken);
await collection.DeleteManyAsync(
Builders<Book>.Filter.Eq(b => b.Type, type),
cancellationToken
);
@ -253,7 +254,7 @@ public async override Task DeleteAsync(
### Access to the MongoDB API
In most cases, you want to hide MongoDB APIs behind a repository (this is the main purpose of the repository). However, if you want to access the MongoDB API over the repository, you can use `GetDatabase()` or `GetCollection()` extension methods. Example:
In most cases, you want to hide MongoDB APIs behind a repository (this is the main purpose of the repository). However, if you want to access the MongoDB API over the repository, you can use `GetDatabaseAsync()`, `GetCollectionAsync()` or `GetAggregateAsync()` extension methods. Example:
```csharp
public class BookService
@ -265,10 +266,11 @@ public class BookService
_bookRepository = bookRepository;
}
public void Foo()
public async Task FooAsync()
{
IMongoDatabase database = _bookRepository.GetDatabase();
IMongoCollection<Book> books = _bookRepository.GetCollection();
IMongoDatabase database = await _bookRepository.GetDatabaseAsync();
IMongoCollection<Book> books = await _bookRepository.GetCollectionAsync();
IAggregateFluent<Book> bookAggregate = await _bookRepository.GetAggregateAsync();
}
}
```
@ -390,36 +392,45 @@ If you have better logic or using an external library for bulk operations, you c
- You may use example template below:
```csharp
public class MyCustomMongoDbBulkOperationProvider : IMongoDbBulkOperationProvider, ITransientDependency
public class MyCustomMongoDbBulkOperationProvider
: IMongoDbBulkOperationProvider, ITransientDependency
{
public async Task DeleteManyAsync<TEntity>(IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
public async Task DeleteManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task InsertManyAsync<TEntity>(IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
public async Task InsertManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task UpdateManyAsync<TEntity>(IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
public async Task UpdateManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
}
```
```
## See Also
* [Entities](Entities.md)
* [Repositories](Repositories.md)

@ -222,7 +222,8 @@ public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPe
public async Task<Person> FindByNameAsync(string name)
{
return await DbContext.Set<Person>()
var dbSet = await GetDbSetAsync();
return await dbSet.Set<Person>()
.Where(p => p.Name == name)
.FirstOrDefaultAsync();
}
@ -235,12 +236,13 @@ You can directly access the data access provider (`DbContext` in this case) to p
## IQueryable & Async Operations
`IRepository` inherits from `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Generic Repositories*" section above.
`IRepository` provides `GetQueryableAsync()` to obtain an `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Querying / LINQ over the Repositories*" section above.
**Example: Using the `Where(...)` and the `ToList()` extension methods**
````csharp
var people = _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
.Where(p => p.Name.Contains(nameFilter))
.ToList();
````
@ -269,7 +271,8 @@ When you add the NuGet package to your project, you can take full power of the E
**Example: Directly using the `ToListAsync()` after adding the EF Core package**
````csharp
var people = _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
.Where(p => p.Name.Contains(nameFilter))
.ToListAsync();
````
@ -285,7 +288,8 @@ If you are using [MongoDB](MongoDB.md), you need to add the [Volo.Abp.MongoDB](h
**Example: Cast `IQueryable<T>` to `IMongoQueryable<T>` and use `ToListAsync()`**
````csharp
var people = ((IMongoQueryable<Person>)_personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = ((IMongoQueryable<Person>) queryable
.Where(p => p.Name.Contains(nameFilter)))
.ToListAsync();
````
@ -312,10 +316,11 @@ The standard LINQ extension methods are supported: *AllAsync, AnyAsync, AverageA
This approach still **has a limitation**. You need to call the extension method directly on the repository object. For example, the below usage is **not supported**:
```csharp
var count = await _bookRepository.Where(x => x.Name.Contains("A")).CountAsync();
var queryable = await _bookRepository.GetQueryableAsync();
var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync();
```
This is because the object returned from the `Where` method is not a repository object, it is a standard `IQueryable` interface. See the other options for such cases.
This is because the `CountAsync()` method in this example is called on a `IQueryable` interface, not on the repository object. See the other options for such cases.
This method is suggested **wherever possible**.
@ -352,8 +357,11 @@ namespace AbpDemo
public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Obtain the IQueryable<T>
var queryable = await _productRepository.GetQueryableAsync();
//Create the query
var query = _productRepository
var query = queryable
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);

@ -18,4 +18,4 @@ You can always check the milestone planning and the prioritized backlog issues o
The backlog items are subject to change. We are adding new items and changing priorities based on the community feedbacks and goals of the project.
Vote for your favorite feature on the related GitHub issues (and write your thoughts). You can create an issue on [the GitHub repository](https://github.com/abpframework/abp) for your feature requests, but first search in in the existing issues.
Vote for your favorite feature on the related GitHub issues (and write your thoughts). You can create an issue on [the GitHub repository](https://github.com/abpframework/abp) for your feature requests, but first search in the existing issues.

@ -122,7 +122,8 @@ namespace MyProject
public async Task<List<Customer>> GetCustomersCanBuyAlcohol()
{
var query = _customerRepository.Where(
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification().ToExpression()
);
@ -137,7 +138,8 @@ namespace MyProject
Actually, using the `ToExpression()` method is not necessary since the specifications are automatically casted to Expressions. This would also work:
````csharp
var query = _customerRepository.Where(
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification()
);
````

@ -364,10 +364,11 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in Repository
var query = from book in queryable
join author in _authorRepository on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
@ -384,17 +385,24 @@ namespace Acme.BookStore.Books
return bookDto;
}
public override async Task<PagedResultDto<BookDto>> GetListAsync(
PagedAndSortedResultRequestDto input)
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in Repository
var query = from book in queryable
join author in _authorRepository on book.AuthorId equals author.Id
orderby input.Sorting
orderby input.Sorting //TODO: Can not sort like that!
select new {book, author};
//Paging
query = query
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
@ -437,7 +445,7 @@ Let's see the changes we've done:
* Injected `IAuthorRepository` to query from the authors.
* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`.
* Used a simple LINQ expression to join books and authors and query them together for the given book id.
* Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. `AsyncExecuter` was previously used in the `AuthorAppService`. Check the [repository documentation](../Repositories.md) to understand why we've used it.
* Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. It is a way to use asynchronous LINQ extensions without depending on the database provider API. Check the [repository documentation](../Repositories.md) to understand why we've used it.
* Throws an `EntityNotFoundException` which results an `HTTP 404` (not found) result if requested book was not present in the database.
* Finally, created a `BookDto` object using the `ObjectMapper`, then assigning the `AuthorName` manually.
* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. The logic is similar to the previous method, so you can easily understand the code.
@ -487,8 +495,6 @@ namespace Acme.BookStore.Books
public async override Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
@ -501,17 +507,18 @@ namespace Acme.BookStore.Books
public async override Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Get the books
var books = await AsyncExecuter.ToListAsync(
Repository
queryable
.OrderBy(input.Sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
@ -553,8 +560,10 @@ namespace Acme.BookStore.Books
.Distinct()
.ToArray();
var queryable = await _authorRepository.GetQueryableAsync();
var authors = await AsyncExecuter.ToListAsync(
_authorRepository.Where(a => authorIds.Contains(a.Id))
queryable.Where(a => authorIds.Contains(a.Id))
);
return authors.ToDictionary(x => x.Id, x => x);

@ -127,7 +127,8 @@ namespace Acme.BookStore.Authors
public async Task<Author> FindByNameAsync(string name)
{
return await DbSet.FirstOrDefaultAsync(author => author.Name == name);
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
@ -136,7 +137,8 @@ namespace Acme.BookStore.Authors
string sorting,
string filter = null)
{
return await DbSet
var dbSet = await GetDbSetAsync();
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
@ -186,8 +188,8 @@ namespace Acme.BookStore.Authors
public async Task<Author> FindByNameAsync(string name)
{
return await GetMongoQueryable()
.FirstOrDefaultAsync(author => author.Name == name);
var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
@ -196,7 +198,8 @@ namespace Acme.BookStore.Authors
string sorting,
string filter = null)
{
return await GetMongoQueryable()
var queryable = await GetMongoQueryableAsync();
return await queryable
.WhereIf<Author, IMongoQueryable<Author>>(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)

@ -172,6 +172,7 @@ using System.Threading.Tasks;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
@ -230,12 +231,10 @@ public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input
input.Filter
);
var totalCount = await AsyncExecuter.CountAsync(
_authorRepository.WhereIf(
!input.Filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(input.Filter)
)
);
var totalCount = input.Filter == null
? await _authorRepository.CountAsync()
: await _authorRepository.CountAsync(
author => author.Name.Contains(input.Filter));
return new PagedResultDto<AuthorDto>(
totalCount,
@ -246,7 +245,7 @@ public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input
* Default sorting is "by author name" which is done in the beginning of the method in case of it wasn't sent by the client.
* Used the `IAuthorRepository.GetListAsync` to get a paged, sorted and filtered list of authors from the database. We had implemented it in the previous part of this tutorial. Again, it actually was not needed to create such a method since we could directly query over the repository, but wanted to demonstrate how to create custom repository methods.
* Directly queried from the `AuthorRepository` while getting the count of the authors. We preferred to use the `AsyncExecuter` service which allows us to perform async queries without depending on the EF Core. However, you could depend on the EF Core package and directly use the `_authorRepository.WhereIf(...).ToListAsync()` method. See the [repository document](../Repositories.md) to read the alternative approaches and the discussion.
* Directly queried from the `AuthorRepository` while getting the count of the authors. If a filter is sent, then we are using it to filter entities while getting the count.
* Finally, returning a paged result by mapping the list of `Author`s to a list of `AuthorDto`s.
### CreateAsync

@ -0,0 +1,83 @@
# Ellipsis
Text inside an HTML element can be truncated easily with an ellipsis by using CSS. To make this even easier, you can use the `EllipsisDirective` which has been exposed by the `@abp/ng.theme.shared` package.
## Getting Started
In order to use the `EllipsisDirective` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this:
```js
// ...
import { ThemeSharedModule } from '@abp/ng.theme.shared';
@NgModule({
//...
imports: [..., ThemeSharedModule],
})
export class MyFeatureModule {}
```
or **if you would not like to import** the `ThemeSharedModule`, you can import the **`EllipsisModule`** as shown below:
```js
// ...
import { EllipsisModule } from '@abp/ng.theme.shared';
@NgModule({
//...
imports: [..., EllipsisModule],
})
export class MyFeatureModule {}
```
## Usage
The `EllipsisDirective` is very easy to use. The directive's selector is **`abpEllipsis`**. By adding the `abpEllipsis` attribute to an HTML element, you can activate the `EllipsisDirective` for the HTML element.
See an example usage:
```html
<p abpEllipsis>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam commodi quae aspernatur,
corporis velit et suscipit id consequuntur amet minima expedita cum reiciendis dolorum
cupiditate? Voluptas eaque voluptatum odio deleniti quo vel illum nemo accusamus nulla ratione
impedit dolorum expedita necessitatibus fugiat ullam beatae, optio eum cupiditate ducimus
architecto.
</p>
```
The `abpEllipsis` attribute has been added to the `<p>` element that containing very long text inside to activate the `EllipsisDirective`.
See the result:
![Ellipsis directive result](./images/ellipsis-directive-result1.jpg)
The long text has been truncated by using the directive.
The UI before using the directive looks like this:
![Before using the EllipsisDirective](./images/ellipsis-directive-before.jpg)
### Specifying Max Width of an HTML Element
An HTML element max width can be specified as shown below:
```html
<div [abpEllipsis]="'100px'">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio!
</div>
<div [abpEllipsis]="'15vw'">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio!
</div>
<div [abpEllipsis]="'50%'">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque, optio!
</div>
```
See the result:
![Ellipsis directive result 2](./images/ellipsis-directive-result2.jpg)

@ -0,0 +1,248 @@
# Modal
`ModalComponent` is a pre-built component exposed by `@abp/ng.theme.shared` package to show modals. The component uses the [`ng-bootstrap`](https://ng-bootstrap.github.io/)'s modal service inside to render a modal.
The `abp-modal` provides some additional benefits:
- It is **flexible**. You can pass header, body, footer templates easily by adding the templates to the `abp-modal` content. It can also be implemented quickly.
- Provides several inputs be able to customize the modal and several outputs be able to listen to some events.
- Automatically detects the close button which has a `#abpClose` template variable and closes the modal when pressed this button.
- Automatically detects the `abp-button` and triggers its loading spinner when the `busy` input value of the modal component is true.
- Automatically checks if the form inside the modal **has changed, but not saved**. It warns the user by displaying a [confirmation popup](Confirmation-Service) in this case when a user tries to close the modal or refresh/close the tab of the browser.
> Note: A modal can also be rendered by using the `ng-bootstrap` modal. For further information, see [Modal doc](https://ng-bootstrap.github.io/#/components/modal) on the `ng-bootstrap` documentation.
## Getting Started
In order to use the `abp-modal` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this:
```js
// ...
import { ThemeSharedModule } from '@abp/ng.theme.shared';
@NgModule({
//...
imports: [..., ThemeSharedModule],
})
export class MyFeatureModule {}
```
## Usage
You can add the `abp-modal` to your component very quickly. See an example:
```html
<!-- sample.component.html -->
<button class="btn btn-primary" (click)="isModalOpen = true">Open modal</button>
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3>Modal Title</h3>
</ng-template>
<ng-template #abpBody>
<p>Modal content</p>
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>Close</button>
</ng-template>
</abp-modal>
```
```js
// sample.component.ts
@Component(/* component metadata */)
export class SampleComponent {
isModelOpen = false
}
```
![Example modal result](./images/modal-result-1.jpg)
See an example form inside a modal:
```html
<!-- book.component.ts -->
<abp-modal [(visible)]="isModalOpen" [busy]="inProgress">
<ng-template #abpHeader>
<h3>Book</h3>
</ng-template>
<ng-template #abpBody>
<form id="book-form" [formGroup]="form" (ngSubmit)="save()">
<div class="form-group">
<label for="book-name">Author</label><span> * </span>
<input type="text" id="author" class="form-control" formControlName="author" autofocus />
</div>
<div class="form-group">
<label for="book-name">Name</label><span> * </span>
<input type="text" id="book-name" class="form-control" formControlName="name" />
</div>
<div class="form-group">
<label for="book-price">Price</label><span> * </span>
<input type="number" id="book-price" class="form-control" formControlName="price" />
</div>
<div class="form-group">
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="0">Undefined</option>
<option [ngValue]="1">Adventure</option>
<option [ngValue]="2">Biography</option>
<option [ngValue]="3">Fantastic</option>
<option [ngValue]="4">Science</option>
</select>
</div>
<div class="form-group">
<label for="book-publish-date">Publish date</label><span> * </span>
<input
id="book-publish-date"
formControlName="publishDate"
class="form-control"
type="date"
/>
</div>
</form>
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
Cancel
</button>
<button form="book-form" class="btn btn-primary" [disabled]="form.invalid || form.pristine">
<i class="fa fa-check mr-1"></i>
Save
</button>
</ng-template>
</abp-modal>
```
```ts
// book.component.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component(/* component metadata */)
export class BookComponent {
form = this.fb.group({
author: [null, [Validators.required]],
name: [null, [Validators.required]],
price: [null, [Validators.required, Validators.min(0)]],
type: [null, [Validators.required]],
publishDate: [null, [Validators.required]],
});
inProgress: boolean;
isModalOpen: boolean;
constructor(private fb: FormBuilder, private service: BookService) {}
save() {
if (this.form.invalid) return;
this.inProgress = true;
this.service.save(this.form.value).subscribe(() => {
this.inProgress = false;
});
}
}
```
The modal with form looks like this:
![Form example result](./images/modal-result-2.jpg)
## API
### Inputs
#### visible
```js
@Input() visible: boolean
```
**`visible`** is a boolean input that determines whether the modal is open. It is also can be used two-way binding.
#### busy
```js
@Input() busy: boolean
```
**`busy`** is a boolean input that determines whether the busy status of the modal is true. When `busy` is true, the modal cannot be closed and the `abp-button` loading spinner is triggered.
#### options
```js
@Input() options: NgbModalOptions
```
**`options`** is an input typed [NgbModalOptions](https://ng-bootstrap.github.io/#/components/modal/api#NgbModalOptions). It is configuration for the `ng-bootstrap` modal.
#### suppressUnsavedChangesWarning
```js
@Input() suppressUnsavedChangesWarning: boolean
```
**`suppressUnsavedChangesWarning`** is a boolean input that determines whether the confirmation popup triggering active or not. It can also be set globally as shown below:
```ts
//app.module.ts
// app.module.ts
import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '@abp/ng.theme.shared';
// ...
@NgModule({
// ...
providers: [{provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true}]
})
export class AppModule {}
```
Note: The `suppressUnsavedChangesWarning` input of `abp-modal` value overrides the `SUPPRESS_UNSAVED_CHANGES_WARNING` injection token value.
### Outputs
#### visibleChange
```js
@Output() readonly visibleChange = new EventEmitter<boolean>();
```
**`visibleChange`** is an event emitted when the modal visibility has changed. The event payload is a boolean.
#### appear
```js
@Output() readonly appear = new EventEmitter<void>();
```
**`appear`** is an event emitted when the modal has opened.
#### disappear
```js
@Output() readonly disappear = new EventEmitter<void>();
```
**`disappear`** is an event emitted when the modal has closed.

@ -0,0 +1,146 @@
# Router Events Simplified
`RouterEvents` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose.
## Benefit
You can use router events directly and filter them as seen below:
```js
import {
NavigationEnd,
NavigationError,
NavigationCancel,
Router,
} from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationFinish$ = this.router.events.pipe(
filter(
event =>
event instanceof NavigationEnd ||
event instanceof NavigationError ||
event instanceof NavigationCancel,
),
);
/* Observable<Event> */
constructor(private router: Router) {}
}
```
However, `RouterEvents` makes filtering router events easier.
```js
import { RouterEvents } from '@abp/ng.core';
@Injectable()
class SomeService {
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel');
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */
constructor(private routerEvents: RouterEvents) {}
}
```
`RouterEvents` also delivers improved type-safety. In the example above, `navigationFinish$` has inferred type of `Observable<NavigationCancel | NavigationEnd | NavigationError>` whereas it would have `Observable<Event>` when router events are filtered directly.
## Usage
You do not have to provide `RouterEvents` at the module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components.
### How to Get Specific Navigation Events
You can use `getNavigationEvents` to get a stream of navigation events matching given event keys.
```js
import { RouterEvents } from '@abp/ng.core';
import { merge } from 'rxjs';
import { mapTo } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationStart$ = this.routerEvents.getNavigationEvents('Start');
/* Observable<NavigationStart> */
navigationFinish$ = this.routerEvents.getNavigationEvents('End', 'Error', 'Cancel');
/* Observable<NavigationCancel | NavigationEnd | NavigationError> */
loading$ = merge(
this.navigationStart$.pipe(mapTo(true)),
this.navigationFinish$.pipe(mapTo(false)),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get All Navigation Events
You can use `getAllNavigationEvents` to get a stream of all navigation events without passing any keys.
```js
import { RouterEvents, NavigationStart } from '@abp/ng.core';
import { map } from 'rxjs/operators';
@Injectable()
class SomeService {
navigationEvent$ = this.routerEvents.getAllNavigationEvents();
/* Observable<NavigationCancel | NavigationEnd | NavigationError | NavigationStart> */
loading$ = this.navigationEvent$.pipe(
map(event => event instanceof NavigationStart),
);
/* Observable<boolean> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get Specific Router Events
You can use `getEvents` to get a stream of router events matching given event constructors.
```js
import { RouterEvents } from '@abp/ng.core';
import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
moduleActivation$ = this.routerEvents.getEvents(ActivationEnd, ChildActivationEnd);
/* Observable<ActivationEnd | ChildActivationEnd> */
constructor(private routerEvents: RouterEvents) {}
}
```
### How to Get All Router Events
You can use `getEvents` to get a stream of all router events without passing any event constructors. This is nothing different from accessing `events` property of `Router` and is added to the service just for convenience.
```js
import { RouterEvents } from '@abp/ng.core';
import { ActivationEnd, ChildActivationEnd } from '@angular/router';
@Injectable()
class SomeService {
routerEvent$ = this.routerEvents.getAllEvents();
/* Observable<Event> */
constructor(private routerEvents: RouterEvents) {}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@ -807,6 +807,10 @@
"text": "Easy *ngFor trackBy",
"path": "UI/Angular/Track-By-Service.md"
},
{
"text": "Router Events",
"path": "UI/Angular/Router-Events.md"
},
{
"text": "Inserting Scripts & Styles to DOM",
"path": "UI/Angular/Dom-Insertion-Service.md"
@ -819,6 +823,10 @@
"text": "Projecting Angular Content",
"path": "UI/Angular/Content-Projection-Service.md"
},
{
"text": "Modal",
"path": "UI/Angular/Modal.md"
},
{
"text": "Confirmation Popup",
"path": "UI/Angular/Confirmation-Service.md"
@ -830,6 +838,10 @@
{
"text": "Page Alerts",
"path": "UI/Angular/Page-Alerts.md"
},
{
"text": "Ellipsis",
"path": "UI/Angular/Ellipsis-Directive.md"
}
]
},
@ -1056,10 +1068,6 @@
{
"text": "CLI",
"path": "CLI.md"
},
{
"text": "API Documentation",
"path": "{ApiDocumentationUrl}"
}
]
},

@ -243,7 +243,7 @@ ObjectExtensionManager.Instance
这是定义实体属性的另一种方法( 有关 `ObjectExtensionManager` 更多信息,请参阅[文档](Object-Extensions.md)). 这次我们设置了 `CheckPairDefinitionOnMapping` 为false,在将实体映射到DTO时会跳过定义检查.
如果你不喜欢这种方法,但想简单的向多个对象(DTO)添加单个属, `AddOrUpdateProperty` 可以使用类型数组添加额外的属性:
如果你不喜欢这种方法,但想简单的向多个对象(DTO)添加单个属, `AddOrUpdateProperty` 可以使用类型数组添加额外的属性:
````csharp
ObjectExtensionManager.Instance

@ -211,7 +211,7 @@ public async override Task DeleteAsync(
#### 访问MongoDB API
大多数情况下,你想要将MongoDB API隐藏在仓储后面(这是仓储的主要目的).如果你想在仓储之上访问MongoDB API,你可以使用`GetDatabase()`或`GetCollection()`方法.例如:
大多数情况下,你想要将MongoDB API隐藏在仓储后面(这是仓储的主要目的).如果你想在仓储之上访问MongoDB API,你可以使用`GetDatabaseAsync()`, `GetAggregateAsync()` 或`GetCollectionAsync()`方法.例如:
```csharp
public class BookService
@ -223,10 +223,11 @@ public class BookService
_bookRepository = bookRepository;
}
public void Foo()
public async Task FooAsync()
{
IMongoDatabase database = _bookRepository.GetDatabase();
IMongoCollection<Book> books = _bookRepository.GetCollection();
IMongoDatabase database = await _bookRepository.GetDatabaseAsync();
IMongoCollection<Book> books = await _bookRepository.GetCollectionAsync();
IAggregateFluent<Book> bookAggregate = await _bookRepository.GetAggregateAsync();
}
}
```

@ -1,7 +1,14 @@
using System.Threading.Tasks;
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.RequestLocalization;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
namespace Volo.Abp.AspNetCore.MultiTenancy
{
@ -21,10 +28,71 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenant = await _tenantConfigurationProvider.GetAsync(saveResolveResult: true);
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
if (tenant?.Id != _currentTenant.Id)
{
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
var requestCulture = await TryGetRequestCultureAsync(context);
if (requestCulture != null)
{
CultureInfo.CurrentCulture = requestCulture.Culture;
CultureInfo.CurrentUICulture = requestCulture.UICulture;
AbpRequestCultureCookieHelper.SetCultureCookie(
context,
requestCulture
);
}
await next(context);
}
}
else
{
await next(context);
}
}
private async Task<RequestCulture> TryGetRequestCultureAsync(HttpContext httpContext)
{
var requestCultureFeature = httpContext.Features.Get<IRequestCultureFeature>();
/* If requestCultureFeature == null, that means the RequestLocalizationMiddleware was not used
* and we don't want to set the culture. */
if (requestCultureFeature == null)
{
return null;
}
/* If requestCultureFeature.Provider is not null, that means RequestLocalizationMiddleware
* already picked a language, so we don't need to set the default. */
if (requestCultureFeature.Provider != null)
{
return null;
}
var settingProvider = httpContext.RequestServices.GetRequiredService<ISettingProvider>();
var defaultLanguage = await settingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage);
if (defaultLanguage.IsNullOrWhiteSpace())
{
return null;
}
string culture;
string uiCulture;
if (defaultLanguage.Contains(';'))
{
var splitted = defaultLanguage.Split(';');
culture = splitted[0];
uiCulture = splitted[1];
}
else
{
culture = defaultLanguage;
uiCulture = defaultLanguage;
}
return new RequestCulture(culture, uiCulture);
}
}
}

@ -7,7 +7,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Client
{
public static string CreateCacheKey(ICurrentUser currentUser)
{
return $"ApplicationConfiguration_{currentUser.Id?.ToString("N") ?? "Anonymous"}_{CultureInfo.CurrentUICulture.Name}";
var userKey = currentUser.Id?.ToString("N") ?? "Anonymous";
return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}";
}
}
}

@ -30,7 +30,7 @@
var htmlEncode = function (html) {
return $('<div/>').text(html).html();
}
var _createDropdownItem = function (record, fieldItem, tableInstance) {
var $li = $('<li/>');
var $a = $('<a/>');
@ -90,15 +90,15 @@
}
$button.append(htmlEncode(firstItem.text));
}
if (firstItem.enabled && !firstItem.enabled({ record: record, table: tableInstance })) {
$button.addClass('disabled');
}
if (firstItem.action) {
$button.click(function (e) {
e.preventDefault();
if (!$(this).hasClass('disabled')) {
if (firstItem.confirmMessage) {
abp.message.confirm(firstItem.confirmMessage({ record: record, table: tableInstance }))
@ -217,7 +217,7 @@
var renderRowActions = function (tableInstance, nRow, aData, iDisplayIndex, iDisplayIndexFull) {
var columns;
if (tableInstance.aoColumns) {
columns = tableInstance.aoColumns;
} else {
@ -463,7 +463,7 @@
datatables.defaultConfigurations.scrollX = true;
datatables.defaultConfigurations.responsive = true;
datatables.defaultConfigurations.responsive = true;
datatables.defaultConfigurations.language = function () {
return {
@ -484,6 +484,6 @@
};
};
datatables.defaultConfigurations.dom = '<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto"i><"col"p>>';
datatables.defaultConfigurations.dom = '<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto mr-auto"i><"col-auto"p>>';
})(jQuery);

@ -27,6 +27,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.RazorPages
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
protected IClock Clock => LazyServiceProvider.LazyGetRequiredService<IClock>();

@ -25,6 +25,7 @@ namespace Volo.Abp.AspNetCore.Mvc
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
protected IUnitOfWorkManager UnitOfWorkManager => LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>();

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Auditing;
using Volo.Abp.AspNetCore.Mvc.Content;
using Volo.Abp.AspNetCore.Mvc.ContentFormatters;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features;

@ -10,6 +10,7 @@ namespace Volo.Abp.AspNetCore.Mvc
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
protected Type ObjectMapperContext { get; set; }

@ -25,17 +25,20 @@ namespace Volo.Abp.AspNetCore.Mvc
{
public ILogger<AspNetCoreApiDescriptionModelProvider> Logger { get; set; }
private readonly AspNetCoreApiDescriptionModelProviderOptions _options;
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider;
private readonly AbpAspNetCoreMvcOptions _options;
private readonly AbpAspNetCoreMvcOptions _abpAspNetCoreMvcOptions;
private readonly AbpApiDescriptionModelOptions _modelOptions;
public AspNetCoreApiDescriptionModelProvider(
IOptions<AspNetCoreApiDescriptionModelProviderOptions> options,
IApiDescriptionGroupCollectionProvider descriptionProvider,
IOptions<AbpAspNetCoreMvcOptions> options,
IOptions<AbpAspNetCoreMvcOptions> abpAspNetCoreMvcOptions,
IOptions<AbpApiDescriptionModelOptions> modelOptions)
{
_descriptionProvider = descriptionProvider;
_options = options.Value;
_descriptionProvider = descriptionProvider;
_abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value;
_modelOptions = modelOptions.Value;
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.Instance;
@ -82,14 +85,14 @@ namespace Volo.Abp.AspNetCore.Mvc
var controllerModel = moduleModel.GetOrAddController(
controllerType.FullName,
CalculateControllerName(controllerType, setting),
_options.ControllerNameGenerator(controllerType, setting),
controllerType,
_modelOptions.IgnoredInterfaces
);
var method = apiDescription.ActionDescriptor.GetMethodInfo();
var uniqueMethodName = GetUniqueActionName(method);
var uniqueMethodName = _options.ActionNameGenerator(method);
if (controllerModel.Actions.ContainsKey(uniqueMethodName))
{
Logger.LogWarning(
@ -119,44 +122,6 @@ namespace Volo.Abp.AspNetCore.Mvc
AddParameterDescriptionsToModel(actionModel, method, apiDescription);
}
private static string CalculateControllerName(Type controllerType, ConventionalControllerSetting setting)
{
var controllerName = controllerType.Name.RemovePostFix("Controller")
.RemovePostFix(ApplicationService.CommonPostfixes);
if (setting?.UrlControllerNameNormalizer != null)
{
controllerName =
setting.UrlControllerNameNormalizer(
new UrlControllerNameNormalizerContext(setting.RootPath, controllerName));
}
return controllerName;
}
private static string GetUniqueActionName(MethodInfo method)
{
var methodNameBuilder = new StringBuilder(method.Name);
var parameters = method.GetParameters();
if (parameters.Any())
{
methodNameBuilder.Append("By");
for (var i = 0; i < parameters.Length; i++)
{
if (i > 0)
{
methodNameBuilder.Append("And");
}
methodNameBuilder.Append(parameters[i].Name.ToPascalCase());
}
}
return methodNameBuilder.ToString();
}
private static List<string> GetSupportedVersions(Type controllerType, MethodInfo method,
ConventionalControllerSetting setting)
{
@ -377,7 +342,7 @@ namespace Volo.Abp.AspNetCore.Mvc
[CanBeNull]
private ConventionalControllerSetting FindSetting(Type controllerType)
{
foreach (var controllerSetting in _options.ConventionalControllers.ConventionalControllerSettings)
foreach (var controllerSetting in _abpAspNetCoreMvcOptions.ConventionalControllers.ConventionalControllerSettings)
{
if (controllerSetting.ControllerTypes.Contains(controllerType))
{

@ -0,0 +1,57 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using Volo.Abp.Application.Services;
using Volo.Abp.AspNetCore.Mvc.Conventions;
namespace Volo.Abp.AspNetCore.Mvc
{
public class AspNetCoreApiDescriptionModelProviderOptions
{
public Func<Type, ConventionalControllerSetting, string> ControllerNameGenerator { get; set; }
public Func<MethodInfo, string> ActionNameGenerator { get; set; }
public AspNetCoreApiDescriptionModelProviderOptions()
{
ControllerNameGenerator = (controllerType, setting) =>
{
var controllerName = controllerType.Name.RemovePostFix("Controller")
.RemovePostFix(ApplicationService.CommonPostfixes);
if (setting?.UrlControllerNameNormalizer != null)
{
controllerName =
setting.UrlControllerNameNormalizer(
new UrlControllerNameNormalizerContext(setting.RootPath, controllerName));
}
return controllerName;
};
ActionNameGenerator = (method) =>
{
var methodNameBuilder = new StringBuilder(method.Name);
var parameters = method.GetParameters();
if (parameters.Any())
{
methodNameBuilder.Append("By");
for (var i = 0; i < parameters.Length; i++)
{
if (i > 0)
{
methodNameBuilder.Append("And");
}
methodNameBuilder.Append(parameters[i].Name.ToPascalCase());
}
}
return methodNameBuilder.ToString();
};
}
}
}

@ -1,25 +0,0 @@
using System.IO;
using Microsoft.AspNetCore.Http;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.Content
{
internal class InternalRemoteStreamContent : IRemoteStreamContent
{
private readonly HttpContext _httpContext;
public InternalRemoteStreamContent(HttpContext httpContext)
{
_httpContext = httpContext;
}
public string ContentType => _httpContext.Request.ContentType;
public long? ContentLength => _httpContext.Request.ContentLength;
public Stream GetStream()
{
return _httpContext.Request.Body;
}
}
}

@ -1,10 +1,10 @@
using System;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.Content
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class RemoteStreamContentInputFormatter : InputFormatter
{
@ -20,9 +20,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Content
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return InputFormatterResult.SuccessAsync(
new InternalRemoteStreamContent(context.HttpContext)
);
return InputFormatterResult.SuccessAsync(new RemoteStreamContent(context.HttpContext.Request.Body)
{
ContentType = context.HttpContext.Request.ContentType
});
}
}
}

@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.Content
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class RemoteStreamContentOutputFormatter : OutputFormatter
{
@ -21,9 +21,16 @@ namespace Volo.Abp.AspNetCore.Mvc.Content
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var remoteStream = (IRemoteStreamContent)context.Object;
using (var stream = remoteStream.GetStream())
if (remoteStream != null)
{
await stream.CopyToAsync(context.HttpContext.Response.Body);
context.HttpContext.Response.ContentType = remoteStream.ContentType;
using (var stream = remoteStream.GetStream())
{
stream.Position = 0;
await stream.CopyToAsync(context.HttpContext.Response.Body);
}
}
}
}

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using System;
using Microsoft.AspNetCore.RequestLocalization;
using Volo.Abp.Localization;
namespace Volo.Abp.AspNetCore.Mvc.Localization
@ -20,12 +20,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Localization
throw new AbpException("Unknown language: " + culture + ". It must be a valid culture!");
}
string cookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture, uiCulture));
Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, cookieValue, new CookieOptions
{
Expires = Clock.Now.AddYears(2)
});
AbpRequestCultureCookieHelper.SetCultureCookie(
HttpContext,
new RequestCulture(culture, uiCulture)
);
if (!string.IsNullOrWhiteSpace(returnUrl))
{

@ -17,6 +17,7 @@ namespace Volo.Abp.AspNetCore.SignalR
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetService<ILoggerFactory>();

@ -0,0 +1,23 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
namespace Microsoft.AspNetCore.RequestLocalization
{
public static class AbpRequestCultureCookieHelper
{
public static void SetCultureCookie(
HttpContext httpContext,
RequestCulture requestCulture)
{
httpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(requestCulture),
new CookieOptions
{
Expires = DateTime.Now.AddYears(2)
}
);
}
}
}

@ -22,7 +22,7 @@ namespace Volo.Abp.Cli.Auth
{
var configuration = new IdentityClientConfiguration(
CliUrls.AccountAbpIo,
"role email abpio abpio_www abpio_commercial offline_access",
"role email abpio abpio_www abpio_commercial offline_access",
"abp-cli",
"1q2w3e*",
OidcConstants.GrantTypes.Password,
@ -43,6 +43,7 @@ namespace Volo.Abp.Cli.Auth
public Task LogoutAsync()
{
FileHelper.DeleteIfExists(CliPaths.AccessToken);
FileHelper.DeleteIfExists(CliPaths.Lic);
return Task.CompletedTask;
}

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
namespace Volo.Abp.Cli
{
@ -10,7 +11,8 @@ namespace Volo.Abp.Cli
public static string Root => Path.Combine(AbpRootPath, "cli");
public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin");
public static string Build => Path.Combine(AbpRootPath, "build");
public static string Lic => Path.Combine(Path.GetTempPath(), Encoding.ASCII.GetString(new byte[] { 65, 98, 112, 76, 105, 99, 101, 110, 115, 101, 46, 98, 105, 110 }));
private static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp");
}
}
}

@ -12,11 +12,13 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Auth;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
using Volo.Abp.Cli.ProjectBuilding.Templates.Console;
using Volo.Abp.Cli.ProjectBuilding.Templates.Microservice;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -31,14 +33,17 @@ namespace Volo.Abp.Cli.Commands
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
public ITemplateInfoProvider TemplateInfoProvider { get; }
public ConnectionStringProvider ConnectionStringProvider { get; }
public NewCommand(TemplateProjectBuilder templateProjectBuilder
, ITemplateInfoProvider templateInfoProvider,
EfCoreMigrationManager efCoreMigrationManager)
EfCoreMigrationManager efCoreMigrationManager,
ConnectionStringProvider connectionStringProvider)
{
_efCoreMigrationManager = efCoreMigrationManager;
TemplateProjectBuilder = templateProjectBuilder;
TemplateInfoProvider = templateInfoProvider;
ConnectionStringProvider = connectionStringProvider;
Logger = NullLogger<NewCommand>.Instance;
}
@ -164,7 +169,7 @@ namespace Volo.Abp.Cli.Commands
databaseManagementSystem != DatabaseManagementSystem.NotSpecified &&
databaseManagementSystem != DatabaseManagementSystem.SQLServer)
{
connectionString = GetNewConnectionStringByDbms(databaseManagementSystem, outputFolder);
connectionString = ConnectionStringProvider.GetByDbms(databaseManagementSystem, outputFolder);
}
commandLineArgs.Options.Add(CliConsts.Command, commandLineArgs.Command);
@ -234,23 +239,9 @@ namespace Volo.Abp.Cli.Commands
var isCommercial = template == AppProTemplate.TemplateName;
OpenThanksPage(uiFramework, databaseProvider, isTiered || commandLineArgs.Options.ContainsKey("separate-identity-server"), isCommercial);
}
}
private string GetNewConnectionStringByDbms(DatabaseManagementSystem databaseManagementSystem, string outputFolder)
{
switch (databaseManagementSystem)
{
case DatabaseManagementSystem.MySQL:
return "Server=localhost;Port=3306;Database=MyProjectName;Uid=root;Pwd=myPassword;";
case DatabaseManagementSystem.PostgreSQL:
return "User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=MyProjectName;Pooling=true;Min Pool Size=0;Max Pool Size=100;Connection Lifetime=0;";
//case DatabaseManagementSystem.Oracle:
case DatabaseManagementSystem.OracleDevart:
return "Data Source=MyProjectName;Integrated Security=yes;";
case DatabaseManagementSystem.SQLite:
return $"Data Source={Path.Combine(outputFolder , "MyProjectName.db")};".Replace("\\", "\\\\");
default:
return null;
else if (MicroserviceTemplateBase.IsMicroserviceTemplate(template))
{
OpenMicroserviceDocumentPage();
}
}
@ -263,19 +254,14 @@ namespace Volo.Abp.Cli.Commands
var tieredYesNo = tiered ? "yes" : "no";
var url = $"https://{urlPrefix}.abp.io/project-created-success?ui={uiFramework:g}&db={databaseProvider:g}&tiered={tieredYesNo}";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
CmdHelper.OpenWebPage(url);
}
private void OpenMicroserviceDocumentPage()
{
var url = "https://docs.abp.io/en/commercial/latest/startup-templates/microservice/index";
CmdHelper.OpenWebPage(url);
}
private bool GetCreateSolutionFolderPreference(CommandLineArgs commandLineArgs)

@ -0,0 +1,30 @@
using System.IO;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands.Services
{
public class ConnectionStringProvider : ITransientDependency
{
public string GetByDbms(DatabaseManagementSystem databaseManagementSystem, string outputFolder = "")
{
switch (databaseManagementSystem)
{
case DatabaseManagementSystem.NotSpecified:
case DatabaseManagementSystem.SQLServer:
return "Server=localhost;Database=MyProjectName;Trusted_Connection=True";
case DatabaseManagementSystem.MySQL:
return "Server=localhost;Port=3306;Database=MyProjectName;Uid=root;Pwd=myPassword;";
case DatabaseManagementSystem.PostgreSQL:
return "User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=MyProjectName;Pooling=true;Min Pool Size=0;Max Pool Size=100;Connection Lifetime=0;";
//case DatabaseManagementSystem.Oracle:
case DatabaseManagementSystem.OracleDevart:
return "Data Source=MyProjectName;Integrated Security=yes;";
case DatabaseManagementSystem.SQLite:
return $"Data Source={Path.Combine(outputFolder , "MyProjectName.db")};".Replace("\\", "\\\\");
default:
return null;
}
}
}
}

@ -21,5 +21,7 @@
public bool AngularUi { get; set; }
public bool MvcUi { get; set; }
public bool BlazorUi { get; set; }
}
}
}

@ -297,7 +297,8 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App
private static void CleanupFolderHierarchy(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
if (context.BuildArgs.UiFramework == UiFramework.Mvc && context.BuildArgs.MobileApp == MobileApp.None)
if ((context.BuildArgs.UiFramework == UiFramework.Mvc || context.BuildArgs.UiFramework == UiFramework.Blazor) &&
context.BuildArgs.MobileApp == MobileApp.None)
{
steps.Add(new MoveFolderStep("/aspnet-core/", "/"));
}

@ -12,6 +12,11 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
{
}
public static bool IsMicroserviceTemplate(string templateName)
{
return templateName == MicroserviceProTemplate.TemplateName;
}
public override IEnumerable<ProjectBuildPipelineStep> GetCustomSteps(ProjectBuildContext context)
{
var steps = new List<ProjectBuildPipelineStep>();

@ -77,6 +77,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Module
private static void UpdateNuGetConfig(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
steps.Add(new UpdateNuGetConfigStep("/aspnet-core/NuGet.Config"));
steps.Add(new UpdateNuGetConfigStep("/NuGet.Config"));
}
private static void ChangeConnectionString(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)

@ -345,11 +345,20 @@ namespace Volo.Abp.Cli.ProjectModification
protected virtual List<string> GetPackageVersionList(JProperty package)
{
var versionListAsJson = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} versions");
var output = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} versions --json");
var versionListAsJson = ExtractVersions(output);
return JsonConvert.DeserializeObject<string[]>(versionListAsJson)
.OrderByDescending(SemanticVersion.Parse, new VersionComparer()).ToList();
}
protected virtual string ExtractVersions(string output)
{
var arrayStart = output.IndexOf('[');
return output.Substring(arrayStart, output.IndexOf(']') - arrayStart + 1);
}
protected virtual bool SpecifiedVersionExists(string version, JProperty package)
{
var versionList = GetPackageVersionList(package);

@ -1,4 +1,5 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Volo.Abp.Cli.Utils
@ -7,6 +8,23 @@ namespace Volo.Abp.Cli.Utils
{
public static int SuccessfulExitCode = 0;
public static void OpenWebPage(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = url.Replace("&", "^&");
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", url);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", url);
}
}
public static void Run(string file, string arguments)
{
var procStartInfo = new ProcessStartInfo(file, arguments);
@ -86,14 +104,28 @@ namespace Volo.Abp.Cli.Utils
public static string GetFileName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
//Windows
return "cmd.exe";
}
//Linux or OSX
if (File.Exists("/bin/bash"))
{
//TODO: Test this. it should work for both operation systems.
return "/bin/bash";
}
//Windows default.
return "cmd.exe";
if (File.Exists("/bin/sh"))
{
return "/bin/sh"; //some Linux distributions like Alpine doesn't have bash
}
throw new AbpException($"Cannot determine shell command for this OS! " +
$"Running on OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription} | " +
$"OS Architecture: {System.Runtime.InteropServices.RuntimeInformation.OSArchitecture} | " +
$"Framework: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription} | " +
$"Process Architecture{System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture}");
}
}
}

@ -35,6 +35,7 @@ namespace Volo.Abp.Application.Services
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
public static string[] CommonPostfixes { get; set; } = { "AppService", "ApplicationService", "Service" };
@ -51,7 +52,7 @@ namespace Volo.Abp.Application.Services
? provider.GetRequiredService<IObjectMapper>()
: (IObjectMapper) provider.GetRequiredService(typeof(IObjectMapper<>).MakeGenericType(ObjectMapperContext)));
public IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetService<IGuidGenerator>(SimpleGuidGenerator.Instance);
protected IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetService<IGuidGenerator>(SimpleGuidGenerator.Instance);
protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>();

@ -1,7 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
@ -15,6 +13,17 @@ namespace Volo.Abp.Domain.Entities
/// </summary>
public static class EntityHelper
{
public static bool IsMultiTenant<TEntity>()
where TEntity : IEntity
{
return IsMultiTenant(typeof(TEntity));
}
public static bool IsMultiTenant(Type type)
{
return typeof(IMultiTenant).IsAssignableFrom(type);
}
public static bool EntityEquals(IEntity entity1, IEntity entity2)
{
if (entity1 == null || entity2 == null)

@ -13,11 +13,12 @@ namespace Volo.Abp.Domain.Services
{
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
[Obsolete("Use LazyServiceProvider instead.")]
public IServiceProvider ServiceProvider { get; set; }
protected IClock Clock => LazyServiceProvider.LazyGetRequiredService<IClock>();
public IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetService<IGuidGenerator>(SimpleGuidGenerator.Instance);
protected IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetService<IGuidGenerator>(SimpleGuidGenerator.Instance);
protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService<ILoggerFactory>();

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.2" />
</ItemGroup>
</Project>

@ -1,10 +1,72 @@
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.MultiTenancy;
namespace Microsoft.EntityFrameworkCore
{
public static class AbpModelBuilderExtensions
{
private const string ModelDatabaseProviderAnnotationKey = "_Abp_DatabaseProvider";
private const string ModelMultiTenancySideAnnotationKey = "_Abp_MultiTenancySide";
#region MultiTenancySide
public static void SetMultiTenancySide(
this ModelBuilder modelBuilder,
MultiTenancySides side)
{
modelBuilder.Model.SetAnnotation(ModelMultiTenancySideAnnotationKey, side);
}
public static MultiTenancySides GetMultiTenancySide(this ModelBuilder modelBuilder)
{
var value = modelBuilder.Model[ModelMultiTenancySideAnnotationKey];
if (value == null)
{
return MultiTenancySides.Both;
}
return (MultiTenancySides) value;
}
/// <summary>
/// Returns true if this is a database schema that is used by the host
/// but can also be shared with the tenants.
/// </summary>
public static bool IsHostDatabase(this ModelBuilder modelBuilder)
{
return modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Host);
}
/// <summary>
/// Returns true if this is a database schema that is used by the tenants
/// but can also be shared with the host.
/// </summary>
public static bool IsTenantDatabase(this ModelBuilder modelBuilder)
{
return modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Tenant);
}
/// <summary>
/// Returns true if this is a database schema that is only used by the host
/// and should not contain tenant-only tables.
/// </summary>
public static bool IsHostOnlyDatabase(this ModelBuilder modelBuilder)
{
return modelBuilder.GetMultiTenancySide() == MultiTenancySides.Host;
}
/// <summary>
/// Returns true if this is a database schema that is only used by tenants.
/// and should not contain host-only tables.
/// </summary>
public static bool IsTenantOnlyDatabase(this ModelBuilder modelBuilder)
{
return modelBuilder.GetMultiTenancySide() == MultiTenancySides.Tenant;
}
#endregion
#region DatabaseProvider
public static void SetDatabaseProvider(
this ModelBuilder modelBuilder,
@ -61,7 +123,7 @@ namespace Microsoft.EntityFrameworkCore
{
modelBuilder.SetDatabaseProvider(EfCoreDatabaseProvider.InMemory);
}
public static bool IsUsingInMemory(
this ModelBuilder modelBuilder)
{
@ -73,7 +135,7 @@ namespace Microsoft.EntityFrameworkCore
{
modelBuilder.SetDatabaseProvider(EfCoreDatabaseProvider.Cosmos);
}
public static bool IsUsingCosmos(
this ModelBuilder modelBuilder)
{
@ -85,11 +147,13 @@ namespace Microsoft.EntityFrameworkCore
{
modelBuilder.SetDatabaseProvider(EfCoreDatabaseProvider.Firebird);
}
public static bool IsUsingFirebird(
this ModelBuilder modelBuilder)
{
return modelBuilder.GetDatabaseProvider() == EfCoreDatabaseProvider.Firebird;
}
#endregion
}
}
}

@ -13,6 +13,7 @@ using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
{
@ -21,18 +22,42 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
where TEntity : class, IEntity
{
[Obsolete("Use GetDbContextAsync() method.")]
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
protected virtual TDbContext DbContext => GetDbContext();
[Obsolete("Use GetDbContextAsync() method.")]
DbContext IEfCoreRepository<TEntity>.DbContext => DbContext.As<DbContext>();
DbContext IEfCoreRepository<TEntity>.DbContext => GetDbContext() as DbContext;
async Task<DbContext> IEfCoreRepository<TEntity>.GetDbContextAsync()
{
return await GetDbContextAsync() as DbContext;
}
[Obsolete("Use GetDbContextAsync() method.")]
private TDbContext GetDbContext()
{
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return _dbContextProvider.GetDbContext();
}
}
return _dbContextProvider.GetDbContext();
}
protected virtual Task<TDbContext> GetDbContextAsync()
{
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return _dbContextProvider.GetDbContextAsync();
}
}
return _dbContextProvider.GetDbContextAsync();
}

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Data;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.EntityFrameworkCore.DependencyInjection
{
@ -86,16 +87,34 @@ namespace Volo.Abp.EntityFrameworkCore.DependencyInjection
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
//Use DefaultConnectionStringResolver.Resolve when we remove IConnectionStringResolver.Resolve
#pragma warning disable 618
var connectionString = serviceProvider.GetRequiredService<IConnectionStringResolver>().Resolve(connectionStringName);
#pragma warning restore 618
var connectionString = ResolveConnectionString<TDbContext>(serviceProvider, connectionStringName);
return new DbContextCreationContext(
connectionStringName,
connectionString
);
}
private static string ResolveConnectionString<TDbContext>(
IServiceProvider serviceProvider,
string connectionStringName)
{
// Use DefaultConnectionStringResolver.Resolve when we remove IConnectionStringResolver.Resolve
#pragma warning disable 618
var connectionStringResolver = serviceProvider.GetRequiredService<IConnectionStringResolver>();
var currentTenant = serviceProvider.GetRequiredService<ICurrentTenant>();
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (currentTenant.Change(null))
{
return connectionStringResolver.Resolve(connectionStringName);
}
}
return connectionStringResolver.Resolve(connectionStringName);
#pragma warning restore 618
}
}
}

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.EntityFrameworkCore
{
@ -14,22 +15,22 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
public IEfCoreDbContext StarterDbContext { get; }
public List<IEfCoreDbContext> AttendedDbContexts { get; }
public EfCoreTransactionApi(IDbContextTransaction dbContextTransaction, IEfCoreDbContext starterDbContext)
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public EfCoreTransactionApi(
IDbContextTransaction dbContextTransaction,
IEfCoreDbContext starterDbContext,
ICancellationTokenProvider cancellationTokenProvider)
{
DbContextTransaction = dbContextTransaction;
StarterDbContext = starterDbContext;
CancellationTokenProvider = cancellationTokenProvider;
AttendedDbContexts = new List<IEfCoreDbContext>();
}
public Task CommitAsync()
{
Commit();
return Task.CompletedTask;
}
protected void Commit()
public async Task CommitAsync()
{
DbContextTransaction.Commit();
await DbContextTransaction.CommitAsync(CancellationTokenProvider.Token);
foreach (var dbContext in AttendedDbContexts)
{
@ -38,7 +39,7 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
continue; //Relational databases use the shared transaction
}
dbContext.Database.CommitTransaction();
await dbContext.Database.CommitTransactionAsync(CancellationTokenProvider.Token);
}
}
@ -47,15 +48,19 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
DbContextTransaction.Dispose();
}
public void Rollback()
public async Task RollbackAsync(CancellationToken cancellationToken)
{
DbContextTransaction.Rollback();
}
await DbContextTransaction.RollbackAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken));
public Task RollbackAsync(CancellationToken cancellationToken)
{
DbContextTransaction.Rollback();
return Task.CompletedTask;
foreach (var dbContext in AttendedDbContexts)
{
if (dbContext.As<DbContext>().HasRelationalTransactionManager())
{
continue; //Relational databases use the shared transaction
}
await dbContext.Database.RollbackTransactionAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken));
}
}
}
}
}

@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.EntityFrameworkCore
@ -23,15 +24,18 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly ICancellationTokenProvider _cancellationTokenProvider;
private readonly ICurrentTenant _currentTenant;
public UnitOfWorkDbContextProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver,
ICancellationTokenProvider cancellationTokenProvider)
ICancellationTokenProvider cancellationTokenProvider,
ICurrentTenant currentTenant)
{
_unitOfWorkManager = unitOfWorkManager;
_connectionStringResolver = connectionStringResolver;
_cancellationTokenProvider = cancellationTokenProvider;
_currentTenant = currentTenant;
Logger = NullLogger<UnitOfWorkDbContextProvider<TDbContext>>.Instance;
}
@ -57,7 +61,7 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
var connectionString = _connectionStringResolver.Resolve(connectionStringName);
var connectionString = ResolveConnectionString(connectionStringName);
var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";
@ -79,7 +83,7 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
var connectionString = await _connectionStringResolver.ResolveAsync(connectionStringName);
var connectionString = await ResolveConnectionStringAsync(connectionStringName);
var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";
@ -168,7 +172,8 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
transactionApiKey,
new EfCoreTransactionApi(
dbtransaction,
dbContext
dbContext,
_cancellationTokenProvider
)
);
@ -186,7 +191,10 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
else
{
dbContext.Database.BeginTransaction(); //TODO: Why not using the new created transaction?
/* No need to store the returning IDbContextTransaction for non-relational databases
* since EfCoreTransactionApi will handle the commit/rollback over the DbContext instance.
*/
dbContext.Database.BeginTransaction();
}
activeTransaction.AttendedDbContexts.Add(dbContext);
@ -212,7 +220,8 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
transactionApiKey,
new EfCoreTransactionApi(
dbTransaction,
dbContext
dbContext,
_cancellationTokenProvider
)
);
@ -230,7 +239,10 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
else
{
await dbContext.Database.BeginTransactionAsync(GetCancellationToken()); //TODO: Why not using the new created transaction?
/* No need to store the returning IDbContextTransaction for non-relational databases
* since EfCoreTransactionApi will handle the commit/rollback over the DbContext instance.
*/
await dbContext.Database.BeginTransactionAsync(GetCancellationToken());
}
activeTransaction.AttendedDbContexts.Add(dbContext);
@ -239,6 +251,35 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
}
private async Task<string> ResolveConnectionStringAsync(string connectionStringName)
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return await _connectionStringResolver.ResolveAsync(connectionStringName);
}
}
return await _connectionStringResolver.ResolveAsync(connectionStringName);
}
[Obsolete("Use ResolveConnectionStringAsync method.")]
private string ResolveConnectionString(string connectionStringName)
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return _connectionStringResolver.Resolve(connectionStringName);
}
}
return _connectionStringResolver.Resolve(connectionStringName);
}
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
{
return _cancellationTokenProvider.FallbackToProvider(preferredValue);

@ -1,16 +0,0 @@
using System.IO;
using Volo.Abp.Content;
namespace Volo.Abp.Http.Client.Content
{
internal class ReferencedRemoteStreamContent : RemoteStreamContent
{
private readonly object[] _references;
public ReferencedRemoteStreamContent(Stream stream, params object[] references)
: base(stream)
{
this._references = references;
}
}
}

@ -1,24 +1,38 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Modeling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Tracing;
namespace Volo.Abp.Http.Client.DynamicProxying
{
public class ApiDescriptionFinder : IApiDescriptionFinder, ITransientDependency
{
public ICancellationTokenProvider CancellationTokenProvider { get; set; }
protected IApiDescriptionCache Cache { get; }
public ApiDescriptionFinder(IApiDescriptionCache cache)
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
public ApiDescriptionFinder(
IApiDescriptionCache cache,
IOptions<AbpCorrelationIdOptions> abpCorrelationIdOptions,
ICorrelationIdProvider correlationIdProvider,
ICurrentTenant currentTenant)
{
Cache = cache;
AbpCorrelationIdOptions = abpCorrelationIdOptions.Value;
CorrelationIdProvider = correlationIdProvider;
CurrentTenant = currentTenant;
CancellationTokenProvider = NullCancellationTokenProvider.Instance;
}
@ -71,10 +85,19 @@ namespace Volo.Abp.Http.Client.DynamicProxying
return await Cache.GetAsync(baseUrl, () => GetApiDescriptionFromServerAsync(client, baseUrl));
}
protected virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionFromServerAsync(HttpClient client, string baseUrl)
protected virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionFromServerAsync(
HttpClient client,
string baseUrl)
{
var response = await client.GetAsync(
baseUrl.EnsureEndsWith('/') + "api/abp/api-definition",
var requestMessage = new HttpRequestMessage(
HttpMethod.Get,
baseUrl.EnsureEndsWith('/') + "api/abp/api-definition"
);
AddHeaders(requestMessage);
var response = await client.SendAsync(
requestMessage,
CancellationTokenProvider.Token
);
@ -93,6 +116,30 @@ namespace Volo.Abp.Http.Client.DynamicProxying
return (ApplicationApiDescriptionModel)result;
}
protected virtual void AddHeaders(HttpRequestMessage requestMessage)
{
//CorrelationId
requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get());
//TenantId
if (CurrentTenant.Id.HasValue)
{
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
}
//Culture
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
if (!currentCulture.IsNullOrEmpty())
{
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture));
}
//X-Requested-With
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest");
}
protected virtual bool TypeMatches(MethodParameterApiDescriptionModel actionParameter, ParameterInfo methodParameter)
{
return NormalizeTypeName(actionParameter.TypeAsString) ==

@ -14,7 +14,6 @@ using Volo.Abp.Content;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.Http.Client.Content;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
@ -112,7 +111,10 @@ namespace Volo.Abp.Http.Client.DynamicProxying
/* returning a class that holds a reference to response
* content just to be sure that GC does not dispose of
* it before we finish doing our work with the stream */
return (T)((object)new ReferencedRemoteStreamContent(await responseContent.ReadAsStreamAsync(), responseContent));
return (T)(object)new RemoteStreamContent(await responseContent.ReadAsStreamAsync())
{
ContentType = responseContent.Headers.ContentType?.ToString()
};
}
var stringContent = await responseContent.ReadAsStringAsync();
@ -136,7 +138,13 @@ namespace Volo.Abp.Http.Client.DynamicProxying
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName);
var action = await ApiDescriptionFinder.FindActionAsync(client, remoteServiceConfig.BaseUrl, typeof(TService), invocation.Method);
var action = await ApiDescriptionFinder.FindActionAsync(
client,
remoteServiceConfig.BaseUrl,
typeof(TService),
invocation.Method
);
var apiVersion = GetApiVersionInfo(action);
var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + UrlBuilder.GenerateUrlWithParameters(action, invocation.ArgumentsDictionary, apiVersion);
@ -156,9 +164,11 @@ namespace Volo.Abp.Http.Client.DynamicProxying
)
);
var response = await client.SendAsync(requestMessage,
var response = await client.SendAsync(
requestMessage,
HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/,
GetCancellationToken());
GetCancellationToken()
);
if (!response.IsSuccessStatusCode)
{
@ -196,7 +206,11 @@ namespace Volo.Abp.Http.Client.DynamicProxying
return action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
}
protected virtual void AddHeaders(IAbpMethodInvocation invocation, ActionApiDescriptionModel action, HttpRequestMessage requestMessage, ApiVersionInfo apiVersion)
protected virtual void AddHeaders(
IAbpMethodInvocation invocation,
ActionApiDescriptionModel action,
HttpRequestMessage requestMessage,
ApiVersionInfo apiVersion)
{
//API Version
if (!apiVersion.Version.IsNullOrEmpty())

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories.MemoryDb;
using Volo.Abp.MemoryDb;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Uow.MemoryDb
{
@ -14,17 +15,20 @@ namespace Volo.Abp.Uow.MemoryDb
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly MemoryDatabaseManager _memoryDatabaseManager;
private readonly ICurrentTenant _currentTenant;
public UnitOfWorkMemoryDatabaseProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver,
TMemoryDbContext dbContext,
MemoryDatabaseManager memoryDatabaseManager)
MemoryDatabaseManager memoryDatabaseManager,
ICurrentTenant currentTenant)
{
_unitOfWorkManager = unitOfWorkManager;
_connectionStringResolver = connectionStringResolver;
DbContext = dbContext;
_memoryDatabaseManager = memoryDatabaseManager;
_currentTenant = currentTenant;
}
public Task<TMemoryDbContext> GetDbContextAsync()
@ -72,5 +76,34 @@ namespace Volo.Abp.Uow.MemoryDb
return ((MemoryDbDatabaseApi)databaseApi).Database;
}
private async Task<string> ResolveConnectionStringAsync()
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TMemoryDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return await _connectionStringResolver.ResolveAsync<TMemoryDbContext>();
}
}
return await _connectionStringResolver.ResolveAsync<TMemoryDbContext>();
}
[Obsolete("Use ResolveConnectionStringAsync method.")]
private string ResolveConnectionString()
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TMemoryDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return _connectionStringResolver.Resolve<TMemoryDbContext>();
}
}
return _connectionStringResolver.Resolve<TMemoryDbContext>();
}
}
}

@ -24,6 +24,8 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
IMongoQueryable<TEntity> GetMongoQueryable();
Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync(CancellationToken cancellationToken = default);
Task<IAggregateFluent<TEntity>> GetAggregateAsync(CancellationToken cancellationToken = default);
}
public interface IMongoDbRepository<TEntity, TKey> : IMongoDbRepository<TEntity>, IRepository<TEntity, TKey>

@ -16,6 +16,7 @@ using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Guids;
using Volo.Abp.MongoDB;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Repositories.MongoDB
{
@ -51,10 +52,34 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
[Obsolete("Use GetDbContextAsync method.")]
protected virtual TMongoDbContext DbContext => DbContextProvider.GetDbContext();
protected virtual TMongoDbContext DbContext => GetDbContext();
[Obsolete("Use GetDbContextAsync method.")]
private TMongoDbContext GetDbContext()
{
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return DbContextProvider.GetDbContext();
}
}
return DbContextProvider.GetDbContext();
}
protected Task<TMongoDbContext> GetDbContextAsync(CancellationToken cancellationToken = default)
{
// Multi-tenancy unaware entities should always use the host connection string
if (!EntityHelper.IsMultiTenant<TEntity>())
{
using (CurrentTenant.Change(null))
{
return DbContextProvider.GetDbContextAsync(GetCancellationToken(cancellationToken));
}
}
return DbContextProvider.GetDbContextAsync(GetCancellationToken(cancellationToken));
}
@ -314,16 +339,26 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
public override async Task DeleteManyAsync(
IEnumerable<TEntity> entities,
bool autoSave = false,
CancellationToken cancellationToken = default)
IEnumerable<TEntity> entities,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
var entityArray = entities.ToArray();
var softDeletedEntities = new List<TEntity>();
var hardDeletedEntities = new List<TEntity>();
foreach (var entity in entityArray)
foreach (var entity in entities)
{
await ApplyAbpConceptsForDeletedEntityAsync(entity);
SetNewConcurrencyStamp(entity);
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) && !IsHardDeleted(entity))
{
softDeletedEntities.Add(entity);
}
else
{
hardDeletedEntities.Add(entity);
}
}
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
@ -331,54 +366,57 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
if (BulkOperationProvider != null)
{
await BulkOperationProvider.DeleteManyAsync(this, entityArray, dbContext.SessionHandle, autoSave, cancellationToken);
await BulkOperationProvider.DeleteManyAsync(this, entities.ToArray(), dbContext.SessionHandle, autoSave, cancellationToken);
return;
}
var entitiesCount = entityArray.Count();
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
if (softDeletedEntities.Count > 0)
{
UpdateResult updateResult;
var softDeleteEntitiesCount = softDeletedEntities.Count;
if (dbContext.SessionHandle != null)
{
updateResult = await collection.UpdateManyAsync(
dbContext.SessionHandle,
CreateEntitiesFilter(entityArray),
CreateEntitiesFilter(softDeletedEntities),
Builders<TEntity>.Update.Set(x => ((ISoftDelete)x).IsDeleted, true)
);
}
else
{
updateResult = await collection.UpdateManyAsync(
CreateEntitiesFilter(entityArray),
CreateEntitiesFilter(softDeletedEntities),
Builders<TEntity>.Update.Set(x => ((ISoftDelete)x).IsDeleted, true)
);
}
if (updateResult.MatchedCount < entitiesCount)
if (updateResult.MatchedCount < softDeleteEntitiesCount)
{
ThrowOptimisticConcurrencyException();
}
}
else
if (hardDeletedEntities.Count > 0)
{
DeleteResult deleteResult;
var hardDeletedEntitiesCount = hardDeletedEntities.Count;
if (dbContext.SessionHandle != null)
{
deleteResult = await collection.DeleteManyAsync(
dbContext.SessionHandle,
CreateEntitiesFilter(entityArray)
CreateEntitiesFilter(hardDeletedEntities)
);
}
else
{
deleteResult = await collection.DeleteManyAsync(
CreateEntitiesFilter(entityArray)
CreateEntitiesFilter(hardDeletedEntities)
);
}
if (deleteResult.DeletedCount < entitiesCount)
if (deleteResult.DeletedCount < hardDeletedEntitiesCount)
{
ThrowOptimisticConcurrencyException();
}
@ -473,6 +511,17 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
);
}
public async Task<IAggregateFluent<TEntity>> GetAggregateAsync(CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync(cancellationToken);
var collection = await GetCollectionAsync(cancellationToken);
return ApplyDataFilters(
dbContext.SessionHandle != null
? collection.Aggregate(dbContext.SessionHandle)
: collection.Aggregate());
}
protected virtual bool IsHardDeleted(TEntity entity)
{
var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet<IEntity>;
@ -621,6 +670,22 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
throw new AbpDbConcurrencyException("Database operation expected to affect 1 row but actually affected 0 row. Data may have been modified or deleted since entities were loaded. This exception has been thrown on optimistic concurrency check.");
}
protected virtual IAggregateFluent<TEntity> ApplyDataFilters(IAggregateFluent<TEntity> aggregate)
{
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) && DataFilter.IsEnabled<ISoftDelete>())
{
aggregate = aggregate.Match(e => ((ISoftDelete)e).IsDeleted == false);
}
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) && DataFilter.IsEnabled<IMultiTenant>())
{
var tenantId = CurrentTenant.Id;
aggregate = aggregate.Match(e => ((IMultiTenant)e).TenantId == tenantId);
}
return aggregate;
}
[Obsolete("This method will be removed in future versions.")]
public QueryableExecutionModel GetExecutionModel()
{

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
@ -16,10 +17,10 @@ namespace Volo.Abp.Domain.Repositories
return repository.ToMongoDbRepository().Database;
}
public static Task<IMongoDatabase> GetDatabaseAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
public static Task<IMongoDatabase> GetDatabaseAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository, CancellationToken cancellationToken = default)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetDatabaseAsync();
return repository.ToMongoDbRepository().GetDatabaseAsync(cancellationToken);
}
[Obsolete("Use GetCollectionAsync method.")]
@ -29,10 +30,10 @@ namespace Volo.Abp.Domain.Repositories
return repository.ToMongoDbRepository().Collection;
}
public static Task<IMongoCollection<TEntity>> GetCollectionAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
public static Task<IMongoCollection<TEntity>> GetCollectionAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository, CancellationToken cancellationToken = default)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetCollectionAsync();
return repository.ToMongoDbRepository().GetCollectionAsync(cancellationToken);
}
[Obsolete("Use GetMongoQueryableAsync method.")]
@ -42,10 +43,16 @@ namespace Volo.Abp.Domain.Repositories
return repository.ToMongoDbRepository().GetMongoQueryable();
}
public static Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
public static Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository, CancellationToken cancellationToken = default)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetMongoQueryableAsync();
return repository.ToMongoDbRepository().GetMongoQueryableAsync(cancellationToken);
}
public static Task<IAggregateFluent<TEntity>> GetAggregateAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository, CancellationToken cancellationToken = default)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetAggregateAsync(cancellationToken);
}
public static IMongoDbRepository<TEntity, TKey> ToMongoDbRepository<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.MongoDB
{
@ -9,19 +9,19 @@ namespace Volo.Abp.Uow.MongoDB
{
public IClientSessionHandle SessionHandle { get; }
public MongoDbTransactionApi(IClientSessionHandle sessionHandle)
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public MongoDbTransactionApi(
IClientSessionHandle sessionHandle,
ICancellationTokenProvider cancellationTokenProvider)
{
SessionHandle = sessionHandle;
CancellationTokenProvider = cancellationTokenProvider;
}
public async Task CommitAsync()
{
await SessionHandle.CommitTransactionAsync();
}
protected void Commit()
{
SessionHandle.CommitTransaction();
await SessionHandle.CommitTransactionAsync(CancellationTokenProvider.Token);
}
public void Dispose()
@ -29,14 +29,11 @@ namespace Volo.Abp.Uow.MongoDB
SessionHandle.Dispose();
}
public void Rollback()
{
SessionHandle.AbortTransaction();
}
public async Task RollbackAsync(CancellationToken cancellationToken)
{
await SessionHandle.AbortTransactionAsync(cancellationToken);
await SessionHandle.AbortTransactionAsync(
CancellationTokenProvider.FallbackToProvider(cancellationToken)
);
}
}
}

@ -8,6 +8,7 @@ using MongoDB.Bson;
using MongoDB.Driver;
using Volo.Abp.Data;
using Volo.Abp.MongoDB;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.MongoDB
@ -20,15 +21,18 @@ namespace Volo.Abp.Uow.MongoDB
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly ICancellationTokenProvider _cancellationTokenProvider;
private readonly ICurrentTenant _currentTenant;
public UnitOfWorkMongoDbContextProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver,
ICancellationTokenProvider cancellationTokenProvider)
ICancellationTokenProvider cancellationTokenProvider,
ICurrentTenant currentTenant)
{
_unitOfWorkManager = unitOfWorkManager;
_connectionStringResolver = connectionStringResolver;
_cancellationTokenProvider = cancellationTokenProvider;
_currentTenant = currentTenant;
Logger = NullLogger<UnitOfWorkMongoDbContextProvider<TMongoDbContext>>.Instance;
}
@ -54,7 +58,7 @@ namespace Volo.Abp.Uow.MongoDB
$"A {nameof(IMongoDatabase)} instance can only be created inside a unit of work!");
}
var connectionString = _connectionStringResolver.Resolve<TMongoDbContext>();
var connectionString = ResolveConnectionString();
var dbContextKey = $"{typeof(TMongoDbContext).FullName}_{connectionString}";
var mongoUrl = new MongoUrl(connectionString);
@ -81,7 +85,7 @@ namespace Volo.Abp.Uow.MongoDB
$"A {nameof(IMongoDatabase)} instance can only be created inside a unit of work!");
}
var connectionString = await _connectionStringResolver.ResolveAsync<TMongoDbContext>();
var connectionString = await ResolveConnectionStringAsync();
var dbContextKey = $"{typeof(TMongoDbContext).FullName}_{connectionString}";
var mongoUrl = new MongoUrl(connectionString);
@ -178,7 +182,10 @@ namespace Volo.Abp.Uow.MongoDB
unitOfWork.AddTransactionApi(
transactionApiKey,
new MongoDbTransactionApi(session)
new MongoDbTransactionApi(
session,
_cancellationTokenProvider
)
);
dbContext.ToAbpMongoDbContext().InitializeDatabase(database, client, session);
@ -215,7 +222,10 @@ namespace Volo.Abp.Uow.MongoDB
unitOfWork.AddTransactionApi(
transactionApiKey,
new MongoDbTransactionApi(session)
new MongoDbTransactionApi(
session,
_cancellationTokenProvider
)
);
dbContext.ToAbpMongoDbContext().InitializeDatabase(database, client, session);
@ -228,6 +238,35 @@ namespace Volo.Abp.Uow.MongoDB
return dbContext;
}
private async Task<string> ResolveConnectionStringAsync()
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TMongoDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return await _connectionStringResolver.ResolveAsync<TMongoDbContext>();
}
}
return await _connectionStringResolver.ResolveAsync<TMongoDbContext>();
}
[Obsolete("Use ResolveConnectionStringAsync method.")]
private string ResolveConnectionString()
{
// Multi-tenancy unaware contexts should always use the host connection string
if (typeof(TMongoDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
{
using (_currentTenant.Change(null))
{
return _connectionStringResolver.Resolve<TMongoDbContext>();
}
}
return _connectionStringResolver.Resolve<TMongoDbContext>();
}
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
{
return _cancellationTokenProvider.FallbackToProvider(preferredValue);

@ -5,8 +5,6 @@ namespace Volo.Abp.Uow
{
public interface ISupportsRollback
{
void Rollback();
Task RollbackAsync(CancellationToken cancellationToken);
}
}
}

@ -262,27 +262,6 @@ namespace Volo.Abp.Uow
}
}
protected virtual void RollbackAll()
{
foreach (var databaseApi in GetAllActiveDatabaseApis())
{
try
{
(databaseApi as ISupportsRollback)?.Rollback();
}
catch { }
}
foreach (var transactionApi in GetAllActiveTransactionApis())
{
try
{
(transactionApi as ISupportsRollback)?.Rollback();
}
catch { }
}
}
protected virtual async Task RollbackAllAsync(CancellationToken cancellationToken)
{
foreach (var databaseApi in GetAllActiveDatabaseApis())

@ -0,0 +1,36 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Shouldly;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
[Route("api/remote-stream-content-test")]
public class RemoteStreamContentTestController : AbpController
{
[HttpGet]
[Route("Download")]
public async Task<IRemoteStreamContent> DownloadAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync"));
return new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
};
}
[HttpPost]
[Route("Upload")]
public async Task<string> UploadAsync([FromBody]IRemoteStreamContent streamContent)
{
using (var reader = new StreamReader(streamContent.GetStream()))
{
return await reader.ReadToEndAsync() + ":" + streamContent.ContentType;
}
}
}
}

@ -0,0 +1,37 @@
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class RemoteStreamContentTestController_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task DownloadAsync()
{
var result = await GetResponseAsync("/api/remote-stream-content-test/download");
result.Content.Headers.ContentType?.ToString().ShouldBe("application/rtf");
(await result.Content.ReadAsStringAsync()).ShouldBe("DownloadAsync");
}
[Fact]
public async Task UploadAsync()
{
using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/remote-stream-content-test/upload"))
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("UploadAsync"));
memoryStream.Position = 0;
requestMessage.Content = new StreamContent(memoryStream);
requestMessage.Content.Headers.Add("Content-Type", "application/rtf");
var response = await Client.SendAsync(requestMessage);
(await response.Content.ReadAsStringAsync()).ShouldBe("UploadAsync:application/rtf");
}
}
}
}

@ -3,7 +3,7 @@
"name": "asp.net",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.shared": "^4.2.0-rc.2",
"@abp/aspnetcore.mvc.ui.theme.shared": "^4.2.0",
"highlight.js": "^9.13.1"
},
"devDependencies": {}

@ -2,30 +2,30 @@
# yarn lockfile v1
"@abp/aspnetcore.mvc.ui.theme.shared@^4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-4.2.0-rc.2.tgz#9ff3af53b667e747c06d3f067a0f68a938ac72a8"
integrity sha512-qsJ6UaUxyOJOHxsqVjlUGhCvQUnbXgGzLvFPJZYLpnWm6ZvE1KAgJnQHL46YSllCDT0meVqHt9byjOCRJUMm9Q==
dependencies:
"@abp/aspnetcore.mvc.ui" "~4.2.0-rc.2"
"@abp/bootstrap" "~4.2.0-rc.2"
"@abp/bootstrap-datepicker" "~4.2.0-rc.2"
"@abp/datatables.net-bs4" "~4.2.0-rc.2"
"@abp/font-awesome" "~4.2.0-rc.2"
"@abp/jquery-form" "~4.2.0-rc.2"
"@abp/jquery-validation-unobtrusive" "~4.2.0-rc.2"
"@abp/lodash" "~4.2.0-rc.2"
"@abp/luxon" "~4.2.0-rc.2"
"@abp/malihu-custom-scrollbar-plugin" "~4.2.0-rc.2"
"@abp/select2" "~4.2.0-rc.2"
"@abp/sweetalert" "~4.2.0-rc.2"
"@abp/timeago" "~4.2.0-rc.2"
"@abp/toastr" "~4.2.0-rc.2"
"@abp/aspnetcore.mvc.ui@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-4.2.0-rc.2.tgz#726f45c7ff12e69e970fb89b4adfb15502a11576"
integrity sha512-EzTYQ8XzXIprfK95LyYc1Ci3to3dNlBOSzkkYfYDXkSR5/SyBKckCTd5FL4c7yrs+qBoJMxKWDyWz+pmf6PIJA==
"@abp/aspnetcore.mvc.ui.theme.shared@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-4.2.0.tgz#a72b5e1cefa27e658b7f072a8c2c1b4ee12baef8"
integrity sha512-bgWwBBJA/74kFGWh7O+lzb+inV5LOlYOpDoCNAL0XgQTrutrPNHwsk5ZnGVfhSYrBgk47HGVZDwsqKMCTRn1ig==
dependencies:
"@abp/aspnetcore.mvc.ui" "~4.2.0"
"@abp/bootstrap" "~4.2.0"
"@abp/bootstrap-datepicker" "~4.2.0"
"@abp/datatables.net-bs4" "~4.2.0"
"@abp/font-awesome" "~4.2.0"
"@abp/jquery-form" "~4.2.0"
"@abp/jquery-validation-unobtrusive" "~4.2.0"
"@abp/lodash" "~4.2.0"
"@abp/luxon" "~4.2.0"
"@abp/malihu-custom-scrollbar-plugin" "~4.2.0"
"@abp/select2" "~4.2.0"
"@abp/sweetalert" "~4.2.0"
"@abp/timeago" "~4.2.0"
"@abp/toastr" "~4.2.0"
"@abp/aspnetcore.mvc.ui@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-4.2.0.tgz#9c090f64a33d31962936d8d53b345afeaa9f2ba3"
integrity sha512-Qt3MUZ41vuvnIVEAYxyOm1mflvFwwvKYsEjFBbePBnwaYL7udooiIFxTEm4FBh3EQJOi+8T84rnXqiIXj4Pi8A==
dependencies:
ansi-colors "^4.1.1"
extend-object "^1.0.0"
@ -36,145 +36,145 @@
micromatch "^4.0.2"
path "^0.12.7"
"@abp/bootstrap-datepicker@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-4.2.0-rc.2.tgz#f2a6d013321c98f8c9a7533087360fce9a5fd074"
integrity sha512-n3gLYLL7hHX83gurN5iRkpairrw0oAyg+Bc6SeIbNAdorun9wBJn7IlmvXxGPn1xK5iDKktRSeWJ885fMgkuWg==
"@abp/bootstrap-datepicker@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-4.2.0.tgz#92abebe044a91b94b8a3b85560b501120a0fa500"
integrity sha512-bhEa/+zGVX00vkXGrbKI/hcl9o5Xgqwb27by9ZQqxk9Go4lwsEP/7lrXM49Cg8XZTT3L0/lYclneEMDrqGafaQ==
dependencies:
bootstrap-datepicker "^1.9.0"
"@abp/bootstrap@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-4.2.0-rc.2.tgz#49250657f667f7553f3c2149fda43f7535de989b"
integrity sha512-WYdyDQzgFlltxqdYINAMVWjXiiDAx780fTJEd3YKLSzxmVGBfoW4cfFzw8vr/G/EMYrgAPQtrIS+BRlFAJXXGQ==
"@abp/bootstrap@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-4.2.0.tgz#c7ce82ead40bf0d62a318f67f180663ec76aa53c"
integrity sha512-E1gEX0ct67KFjKiZB6eQcIYJ3TS/pF1S4CxknBVCd77Zs03bHI+eEgBNlwcYriVBbQo+vheUQAaACbi+e2mm3Q==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
bootstrap "^4.5.0"
bootstrap-v4-rtl "4.4.1-2"
"@abp/core@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-4.2.0-rc.2.tgz#dc18c3b8f294563ba62c000c026f5ceb86030456"
integrity sha512-YJz4xUwb/mv/Xi/WuPz2SF/LD22P/UJ+psb+35ezOeuj020IAdaunk8LdrU5TfaZqNe5WpGszKMA+jO9ydNuqA==
"@abp/core@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-4.2.0.tgz#20da3d1bab30e80d8864915fdd67e99db729be71"
integrity sha512-Cfa6Ck+Tr7isVpNxo9qT9eKByLadDErA+QVjeps7qYq9ztIpIz/7Yl85tHYEH0YPO9y2zcSOvxjy5SCBXlph5Q==
dependencies:
"@abp/utils" "^4.2.0-rc.2"
"@abp/utils" "^4.2.0"
"@abp/datatables.net-bs4@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-4.2.0-rc.2.tgz#0d14886aa756ca707dd365b423b40d84c658268e"
integrity sha512-kX+NrGkisy7IMi+xL69dPaPOYNLBZ4Vp7xCvJqNhdc5Sh6Rn1P0bLvxFKG065mBMfgW8tyFNnN3c5WzwcRFSmg==
"@abp/datatables.net-bs4@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-4.2.0.tgz#b5cb47235079a4063c5ff8ce78e65596fa587709"
integrity sha512-N4hp2xCst1qavt07Nf+zXlvbZfeSah64VuMjsZgaimHMlDjxg6zVsTHLgLfzwfPV/eOBrGiecPEuvfEeaqcw4A==
dependencies:
"@abp/datatables.net" "~4.2.0-rc.2"
"@abp/datatables.net" "~4.2.0"
datatables.net-bs4 "^1.10.21"
"@abp/datatables.net@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-4.2.0-rc.2.tgz#a82ac16c1644a92d01ba63bf57e0d3784eb00f2f"
integrity sha512-bEQ6QSUN65lJ8fK8wOVE16cayDI64EgyVb3qZAzWUncxPyLLa5s5zlDjcQ98D3An+x3tG2CdmEEWjlAgWdaf1g==
"@abp/datatables.net@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-4.2.0.tgz#2e0273c3212e5a62fc8b4138e4570fb27143f701"
integrity sha512-33ZkaorkVkPQ7HNtDjyIvcVbvAdFm8V+gFN+xMQZhA10wMlMGrKzVJjriv4NhNpPjtB4/owhOv6wobiuWBrGiA==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
datatables.net "^1.10.21"
"@abp/font-awesome@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-4.2.0-rc.2.tgz#6c72ef7ae00beda565e9836c8214ccf074ea7e4b"
integrity sha512-bEQPGyVsKV7aQtVwG4UIcG+DMneBD7jcjX3eFqjOlGhQq3ODbNld5IPXHO+JnHDLt5NOzrePQLUzPhsvCIwhGA==
"@abp/font-awesome@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-4.2.0.tgz#a637d164634b0df862e4ea620736afae282c71c9"
integrity sha512-P8OXp+XIZj6l7cNJ5+7Lpl8iixI/bHGSPkATggaOYZ2EWoNR3B/8pj7p44weP2bCAvUEvaxk214BKlpAslo8eQ==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
"@fortawesome/fontawesome-free" "^5.13.0"
"@abp/jquery-form@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-4.2.0-rc.2.tgz#a4726794f78cf69dd75c0adda3dca3ed623f02d8"
integrity sha512-rOGXYTTiQUD2HWTabh0+Ot6LX4PEObd3DW3ikBHZSnfen7W0OgnLdqNaunHYSEV3UUgCAZHaX0wQd4SM0pRMLg==
"@abp/jquery-form@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-4.2.0.tgz#12b88a79a69f7d3b15aed8c7ca3ff5bc9f526f22"
integrity sha512-lxvXhA4kg002gmDTWgoE6TcQWDe6kBcpV4KBB7aK/6LjK3noeT29SPX0kvKxjoUKyM5TqT2Hx7rg4jgtICjFFA==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
jquery-form "^4.3.0"
"@abp/jquery-validation-unobtrusive@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-4.2.0-rc.2.tgz#5dee22e1bcbd44bd8e2584d37d79e8313cf615ec"
integrity sha512-xk56Otr6PG/bED2yJuPo0OhwcgS4FQQXySgPD9e7mlZQcjxZ4LvmRU/btoNoxSHKYUBLKRfEOYOqRTxJhrH0eA==
"@abp/jquery-validation-unobtrusive@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-4.2.0.tgz#05bcd303ec1e22b85389d03a8941eb898bac354f"
integrity sha512-mtvUcoTD5XSurMFI5cybgWzFI9bn35vU1PvH5NJxJitjJ7sg9gjtLf9WOIddIy4FdqmprhbG2YnR32GITOwFhg==
dependencies:
"@abp/jquery-validation" "~4.2.0-rc.2"
"@abp/jquery-validation" "~4.2.0"
jquery-validation-unobtrusive "^3.2.11"
"@abp/jquery-validation@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-4.2.0-rc.2.tgz#594b9132d1eafebed60be10d219bc9e8b15f18b6"
integrity sha512-LkNI3X7gpYYkI3DnCTZ1KIV6ykKMqw6pLZhFvTZz7GliZWE1jHtFos4U6lWe2XnMjnFeVeY6rPFM/1LaamfCKg==
"@abp/jquery-validation@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-4.2.0.tgz#a09d7cf2abf8ccf0c59f27eadacb030fb2067bbe"
integrity sha512-/nvJrs1pt3LJX+SgH3FtDPfCDrcl4M0LDxjV7hf0Y1jtyhvWOQiyJMb8FKzMweTTEOd0pHG/GvOKd90STabSiw==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
jquery-validation "^1.19.2"
"@abp/jquery@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-4.2.0-rc.2.tgz#146589cbf4397be727d38798d557f3879ae51057"
integrity sha512-yTYk5rRG3qnRKnA/pSXZNH/+KJcJ2Wo5BUtt3G8oKAe+no0Au1BoCV9j7JNiSnLXkFVWFuRwEZCh+cp6ngACng==
"@abp/jquery@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-4.2.0.tgz#0a24488198ca6c9b84273fa028b853ee9f94f3a7"
integrity sha512-6a42Iy7knhgzvQUvCHenrVnPDKhsOyqgZkPxs9pa9x/wNapSch+jLM7u1ezKAFj/Ai4w9yG/yqP+4YE8seZDZw==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
jquery "~3.5.1"
"@abp/lodash@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-4.2.0-rc.2.tgz#f0a238991c288f439e2751938a39d434dd7883e7"
integrity sha512-En9gZD8TDY8ZvZqHVsv3cP2EsRdPRsGD0ncUYOvlNG/sc+MZpRmZ+xOLws1wXS83IxGAxzPdCFEu0biJlyOyxg==
"@abp/lodash@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-4.2.0.tgz#74dfac7d70f1563df2b78c5ebd0b2450e11c4c42"
integrity sha512-5hpjxWZJPvjMIY7FCCw/v/B+JzJbB/yuoioRzcU4vrisY9uRq54vVYcX7hH1DldsJFPTuXY3Id1WBflHA5I9Aw==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
lodash "^4.17.15"
"@abp/luxon@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-4.2.0-rc.2.tgz#ace30caacf6abcce1559b65046ea55e47e1cb8a2"
integrity sha512-ivhOadlaeBh6Pi3S5Bo0pb4EPCEjMFL5N5tZXGGieZt7EnF+4uiXbKJVErXNCDtX1jhJT/3HGqmdxa+WynUE0A==
"@abp/luxon@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-4.2.0.tgz#9cd02faa02cda1315f3e23ae204991ef47d6ae7c"
integrity sha512-dwtv2kqWCDyQADA1Os0aIy6Au2PhBtN6q9kukLWCvYmQZI23OKuEBMSngbJVTqEPfz0LV6CWbsWCUC3okAs46A==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
luxon "^1.24.1"
"@abp/malihu-custom-scrollbar-plugin@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-4.2.0-rc.2.tgz#483668c09e13b0ebcea16320a16f8517fcdd53ee"
integrity sha512-IYOwJcWyJkn/gdp4VkEYCv2Gi5+Boxe7pE7hA49gxgllHiFV0ABDFrRg4h8MbyJd80iwvVPo8t8ouvn4Snm+gQ==
"@abp/malihu-custom-scrollbar-plugin@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-4.2.0.tgz#2f8538df5567a2cf227998e4e2fb9a44b6e814eb"
integrity sha512-3D5REdR7yw2sRRfm3Oi+qlhDABLrKvfU/l7JcCJy+vrBvadx3e3pTdTLXsGptypPN3x/Qr400LgwIrrgyefN8g==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
malihu-custom-scrollbar-plugin "^3.1.5"
"@abp/select2@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-4.2.0-rc.2.tgz#d11e808dac90e09e14025b6ba6dabed683a9b1fb"
integrity sha512-fENomkoAML+NQWvZ/ecjvqnXtvrzJKyChwj9Ul12c+Noi6boeuFDbhi/OG6qCrSzDnNBrehHVXBLFxT9sIL0OA==
"@abp/select2@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-4.2.0.tgz#eb05d22b1390c037911ba25e5ca0f2ed508a93b1"
integrity sha512-VlNoa9+F1/kGmaEI2wbL/cRNeAEpp0UdLpbadAnsmpUIODxCULCWS556Q4Y6Ff4CzYtzkYz2qaJ8T8pn+3EOcA==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
select2 "^4.0.13"
"@abp/sweetalert@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-4.2.0-rc.2.tgz#2359eeb9e4b34c4cbb3aaed11136ea3d280bd677"
integrity sha512-oQPj2vKPNAOvBECgJFdDwvJ8nwQ54ZPsAW1XS3XSkFz3YwWn4Xt9eZK6vecHloYckSckrFtsN/DSQI8ao/uIFg==
"@abp/sweetalert@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-4.2.0.tgz#b1daf641f27f0c8c2e246d364d6285c133faf773"
integrity sha512-AarV/L031xNB1gk/OYEUxkKZmls7zTwovS2t3ZrmwXXoav7nBCSPjEf1ByR2yhZRU/Yo1SNY/CNQQ4dgyf7Xng==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
sweetalert "^2.1.2"
"@abp/timeago@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-4.2.0-rc.2.tgz#d243acff635978068ec579c83a34d0680616c5ef"
integrity sha512-TF1dt9ro3BQnfVetDzbOkedSIncSwvPfnHKMY1xcLG2h0c6ke2PjDQ+4QJrtigggT4vfgmQtRrs/zJxjzWS7qw==
"@abp/timeago@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-4.2.0.tgz#9b15c496b7e63df93921d41de9e9eaf35a8dccbe"
integrity sha512-hyuLVGluHxtWEhR0VakPx5UCbyPcfq4lECEgHhI62PvH0xcSNNZ3bp1QnBTd3se/HAYvwA5sCwJY0YXQgUF2UQ==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
timeago "^1.6.7"
"@abp/toastr@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-4.2.0-rc.2.tgz#4e76f4b7068f92b267b725ad597b8315e6b615bc"
integrity sha512-UJVuQWc7py54JC35RrH+MwAZsOoOhejxX+3aonz8Io2rgFFj0YwK5U+vItRNM3iabSqTcIExcSD3XL2TmQyVzg==
"@abp/toastr@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-4.2.0.tgz#400e2f28b19c72b131ca32aa188ff5151d035752"
integrity sha512-tuGZd3kXqj1t/Y1xN8sOUH1KxiArYKD7xkhuajoOS65Ex5ad8mHPuzCtu8Pv+KL8TVFys9x3U3Tg24DQ6ASjhQ==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
toastr "^2.1.4"
"@abp/utils@^4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.2.0-rc.2.tgz#43c27d421da8e58a5d74c0a2df8742efa827c842"
integrity sha512-6gSLTP9s88aFjxm+fU2iZMzv5Eoe5/va2PHTAoQJA4BaP/SpFRGts3cT4HYWPfAbmzZVZ+tvjuYd3P6d3FCAhw==
"@abp/utils@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.2.0.tgz#073330e3e3f6ee61892f50260e48dbe1b05df35e"
integrity sha512-75qR3SdiAa75wqleAx9sUMXLj4m9duuBo5+2sVv7Y29GcEyKUPvm8B1s6tksvSGA0e3vnFTHeVEc10eD1xKHSQ==
dependencies:
just-compare "^1.3.0"

@ -3,8 +3,8 @@
"name": "asp.net",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.basic": "^4.2.0-rc.2",
"@abp/prismjs": "^4.2.0-rc.2"
"@abp/aspnetcore.mvc.ui.theme.basic": "^4.2.0",
"@abp/prismjs": "^4.2.0"
},
"devDependencies": {}
}

@ -2,37 +2,37 @@
# yarn lockfile v1
"@abp/aspnetcore.mvc.ui.theme.basic@^4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-4.2.0-rc.2.tgz#f4625e18fd3704d432913e9de59c7183770ea485"
integrity sha512-wwOFcrICyhgAcBq7zgdIkYQ8Mov0g1iYrjoRZRiUzBYG4wkcAO9POM9asE6uvY5sD/hd0cRoXSZOcNKUt0J1SA==
dependencies:
"@abp/aspnetcore.mvc.ui.theme.shared" "~4.2.0-rc.2"
"@abp/aspnetcore.mvc.ui.theme.shared@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-4.2.0-rc.2.tgz#9ff3af53b667e747c06d3f067a0f68a938ac72a8"
integrity sha512-qsJ6UaUxyOJOHxsqVjlUGhCvQUnbXgGzLvFPJZYLpnWm6ZvE1KAgJnQHL46YSllCDT0meVqHt9byjOCRJUMm9Q==
dependencies:
"@abp/aspnetcore.mvc.ui" "~4.2.0-rc.2"
"@abp/bootstrap" "~4.2.0-rc.2"
"@abp/bootstrap-datepicker" "~4.2.0-rc.2"
"@abp/datatables.net-bs4" "~4.2.0-rc.2"
"@abp/font-awesome" "~4.2.0-rc.2"
"@abp/jquery-form" "~4.2.0-rc.2"
"@abp/jquery-validation-unobtrusive" "~4.2.0-rc.2"
"@abp/lodash" "~4.2.0-rc.2"
"@abp/luxon" "~4.2.0-rc.2"
"@abp/malihu-custom-scrollbar-plugin" "~4.2.0-rc.2"
"@abp/select2" "~4.2.0-rc.2"
"@abp/sweetalert" "~4.2.0-rc.2"
"@abp/timeago" "~4.2.0-rc.2"
"@abp/toastr" "~4.2.0-rc.2"
"@abp/aspnetcore.mvc.ui@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-4.2.0-rc.2.tgz#726f45c7ff12e69e970fb89b4adfb15502a11576"
integrity sha512-EzTYQ8XzXIprfK95LyYc1Ci3to3dNlBOSzkkYfYDXkSR5/SyBKckCTd5FL4c7yrs+qBoJMxKWDyWz+pmf6PIJA==
"@abp/aspnetcore.mvc.ui.theme.basic@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-4.2.0.tgz#01d6aab8a31fd6ea828b753805f1ba9a9165975f"
integrity sha512-d+7YubPbuBRY8tjqBxS4I1gyxekjg8Z0X9QaDrf/SGQBoWpIdbG09AekNdddKNkimWcPg4UgUytahPuX9f17ZA==
dependencies:
"@abp/aspnetcore.mvc.ui.theme.shared" "~4.2.0"
"@abp/aspnetcore.mvc.ui.theme.shared@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-4.2.0.tgz#a72b5e1cefa27e658b7f072a8c2c1b4ee12baef8"
integrity sha512-bgWwBBJA/74kFGWh7O+lzb+inV5LOlYOpDoCNAL0XgQTrutrPNHwsk5ZnGVfhSYrBgk47HGVZDwsqKMCTRn1ig==
dependencies:
"@abp/aspnetcore.mvc.ui" "~4.2.0"
"@abp/bootstrap" "~4.2.0"
"@abp/bootstrap-datepicker" "~4.2.0"
"@abp/datatables.net-bs4" "~4.2.0"
"@abp/font-awesome" "~4.2.0"
"@abp/jquery-form" "~4.2.0"
"@abp/jquery-validation-unobtrusive" "~4.2.0"
"@abp/lodash" "~4.2.0"
"@abp/luxon" "~4.2.0"
"@abp/malihu-custom-scrollbar-plugin" "~4.2.0"
"@abp/select2" "~4.2.0"
"@abp/sweetalert" "~4.2.0"
"@abp/timeago" "~4.2.0"
"@abp/toastr" "~4.2.0"
"@abp/aspnetcore.mvc.ui@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-4.2.0.tgz#9c090f64a33d31962936d8d53b345afeaa9f2ba3"
integrity sha512-Qt3MUZ41vuvnIVEAYxyOm1mflvFwwvKYsEjFBbePBnwaYL7udooiIFxTEm4FBh3EQJOi+8T84rnXqiIXj4Pi8A==
dependencies:
ansi-colors "^4.1.1"
extend-object "^1.0.0"
@ -43,162 +43,162 @@
micromatch "^4.0.2"
path "^0.12.7"
"@abp/bootstrap-datepicker@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-4.2.0-rc.2.tgz#f2a6d013321c98f8c9a7533087360fce9a5fd074"
integrity sha512-n3gLYLL7hHX83gurN5iRkpairrw0oAyg+Bc6SeIbNAdorun9wBJn7IlmvXxGPn1xK5iDKktRSeWJ885fMgkuWg==
"@abp/bootstrap-datepicker@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-4.2.0.tgz#92abebe044a91b94b8a3b85560b501120a0fa500"
integrity sha512-bhEa/+zGVX00vkXGrbKI/hcl9o5Xgqwb27by9ZQqxk9Go4lwsEP/7lrXM49Cg8XZTT3L0/lYclneEMDrqGafaQ==
dependencies:
bootstrap-datepicker "^1.9.0"
"@abp/bootstrap@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-4.2.0-rc.2.tgz#49250657f667f7553f3c2149fda43f7535de989b"
integrity sha512-WYdyDQzgFlltxqdYINAMVWjXiiDAx780fTJEd3YKLSzxmVGBfoW4cfFzw8vr/G/EMYrgAPQtrIS+BRlFAJXXGQ==
"@abp/bootstrap@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-4.2.0.tgz#c7ce82ead40bf0d62a318f67f180663ec76aa53c"
integrity sha512-E1gEX0ct67KFjKiZB6eQcIYJ3TS/pF1S4CxknBVCd77Zs03bHI+eEgBNlwcYriVBbQo+vheUQAaACbi+e2mm3Q==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
bootstrap "^4.5.0"
bootstrap-v4-rtl "4.4.1-2"
"@abp/clipboard@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-4.2.0-rc.2.tgz#0cda5f6f3624e68e1ec7290517364d02f2b60e42"
integrity sha512-wF/d8Xuq+ORUkiWgDorM7rxueiygtELaZKlRDYzmQRnwJN4vS1q/4/UtPaLSlcGhHxWuxu2XSWQtzcphfBjFWA==
"@abp/clipboard@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-4.2.0.tgz#860220df1d57ef64e864401d56cb5cf7068d5861"
integrity sha512-yBgMDhpqPEHp9N9//Ur1BEIOicFIoKBut75PMz9XPOttLyqCmqHYEPj/jgAkUzLd5O+fJM9TE3yGNM7YdRbRPg==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
clipboard "^2.0.6"
"@abp/core@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-4.2.0-rc.2.tgz#dc18c3b8f294563ba62c000c026f5ceb86030456"
integrity sha512-YJz4xUwb/mv/Xi/WuPz2SF/LD22P/UJ+psb+35ezOeuj020IAdaunk8LdrU5TfaZqNe5WpGszKMA+jO9ydNuqA==
"@abp/core@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-4.2.0.tgz#20da3d1bab30e80d8864915fdd67e99db729be71"
integrity sha512-Cfa6Ck+Tr7isVpNxo9qT9eKByLadDErA+QVjeps7qYq9ztIpIz/7Yl85tHYEH0YPO9y2zcSOvxjy5SCBXlph5Q==
dependencies:
"@abp/utils" "^4.2.0-rc.2"
"@abp/utils" "^4.2.0"
"@abp/datatables.net-bs4@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-4.2.0-rc.2.tgz#0d14886aa756ca707dd365b423b40d84c658268e"
integrity sha512-kX+NrGkisy7IMi+xL69dPaPOYNLBZ4Vp7xCvJqNhdc5Sh6Rn1P0bLvxFKG065mBMfgW8tyFNnN3c5WzwcRFSmg==
"@abp/datatables.net-bs4@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-4.2.0.tgz#b5cb47235079a4063c5ff8ce78e65596fa587709"
integrity sha512-N4hp2xCst1qavt07Nf+zXlvbZfeSah64VuMjsZgaimHMlDjxg6zVsTHLgLfzwfPV/eOBrGiecPEuvfEeaqcw4A==
dependencies:
"@abp/datatables.net" "~4.2.0-rc.2"
"@abp/datatables.net" "~4.2.0"
datatables.net-bs4 "^1.10.21"
"@abp/datatables.net@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-4.2.0-rc.2.tgz#a82ac16c1644a92d01ba63bf57e0d3784eb00f2f"
integrity sha512-bEQ6QSUN65lJ8fK8wOVE16cayDI64EgyVb3qZAzWUncxPyLLa5s5zlDjcQ98D3An+x3tG2CdmEEWjlAgWdaf1g==
"@abp/datatables.net@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-4.2.0.tgz#2e0273c3212e5a62fc8b4138e4570fb27143f701"
integrity sha512-33ZkaorkVkPQ7HNtDjyIvcVbvAdFm8V+gFN+xMQZhA10wMlMGrKzVJjriv4NhNpPjtB4/owhOv6wobiuWBrGiA==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
datatables.net "^1.10.21"
"@abp/font-awesome@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-4.2.0-rc.2.tgz#6c72ef7ae00beda565e9836c8214ccf074ea7e4b"
integrity sha512-bEQPGyVsKV7aQtVwG4UIcG+DMneBD7jcjX3eFqjOlGhQq3ODbNld5IPXHO+JnHDLt5NOzrePQLUzPhsvCIwhGA==
"@abp/font-awesome@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-4.2.0.tgz#a637d164634b0df862e4ea620736afae282c71c9"
integrity sha512-P8OXp+XIZj6l7cNJ5+7Lpl8iixI/bHGSPkATggaOYZ2EWoNR3B/8pj7p44weP2bCAvUEvaxk214BKlpAslo8eQ==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
"@fortawesome/fontawesome-free" "^5.13.0"
"@abp/jquery-form@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-4.2.0-rc.2.tgz#a4726794f78cf69dd75c0adda3dca3ed623f02d8"
integrity sha512-rOGXYTTiQUD2HWTabh0+Ot6LX4PEObd3DW3ikBHZSnfen7W0OgnLdqNaunHYSEV3UUgCAZHaX0wQd4SM0pRMLg==
"@abp/jquery-form@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-4.2.0.tgz#12b88a79a69f7d3b15aed8c7ca3ff5bc9f526f22"
integrity sha512-lxvXhA4kg002gmDTWgoE6TcQWDe6kBcpV4KBB7aK/6LjK3noeT29SPX0kvKxjoUKyM5TqT2Hx7rg4jgtICjFFA==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
jquery-form "^4.3.0"
"@abp/jquery-validation-unobtrusive@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-4.2.0-rc.2.tgz#5dee22e1bcbd44bd8e2584d37d79e8313cf615ec"
integrity sha512-xk56Otr6PG/bED2yJuPo0OhwcgS4FQQXySgPD9e7mlZQcjxZ4LvmRU/btoNoxSHKYUBLKRfEOYOqRTxJhrH0eA==
"@abp/jquery-validation-unobtrusive@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-4.2.0.tgz#05bcd303ec1e22b85389d03a8941eb898bac354f"
integrity sha512-mtvUcoTD5XSurMFI5cybgWzFI9bn35vU1PvH5NJxJitjJ7sg9gjtLf9WOIddIy4FdqmprhbG2YnR32GITOwFhg==
dependencies:
"@abp/jquery-validation" "~4.2.0-rc.2"
"@abp/jquery-validation" "~4.2.0"
jquery-validation-unobtrusive "^3.2.11"
"@abp/jquery-validation@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-4.2.0-rc.2.tgz#594b9132d1eafebed60be10d219bc9e8b15f18b6"
integrity sha512-LkNI3X7gpYYkI3DnCTZ1KIV6ykKMqw6pLZhFvTZz7GliZWE1jHtFos4U6lWe2XnMjnFeVeY6rPFM/1LaamfCKg==
"@abp/jquery-validation@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-4.2.0.tgz#a09d7cf2abf8ccf0c59f27eadacb030fb2067bbe"
integrity sha512-/nvJrs1pt3LJX+SgH3FtDPfCDrcl4M0LDxjV7hf0Y1jtyhvWOQiyJMb8FKzMweTTEOd0pHG/GvOKd90STabSiw==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
jquery-validation "^1.19.2"
"@abp/jquery@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-4.2.0-rc.2.tgz#146589cbf4397be727d38798d557f3879ae51057"
integrity sha512-yTYk5rRG3qnRKnA/pSXZNH/+KJcJ2Wo5BUtt3G8oKAe+no0Au1BoCV9j7JNiSnLXkFVWFuRwEZCh+cp6ngACng==
"@abp/jquery@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-4.2.0.tgz#0a24488198ca6c9b84273fa028b853ee9f94f3a7"
integrity sha512-6a42Iy7knhgzvQUvCHenrVnPDKhsOyqgZkPxs9pa9x/wNapSch+jLM7u1ezKAFj/Ai4w9yG/yqP+4YE8seZDZw==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
jquery "~3.5.1"
"@abp/lodash@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-4.2.0-rc.2.tgz#f0a238991c288f439e2751938a39d434dd7883e7"
integrity sha512-En9gZD8TDY8ZvZqHVsv3cP2EsRdPRsGD0ncUYOvlNG/sc+MZpRmZ+xOLws1wXS83IxGAxzPdCFEu0biJlyOyxg==
"@abp/lodash@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-4.2.0.tgz#74dfac7d70f1563df2b78c5ebd0b2450e11c4c42"
integrity sha512-5hpjxWZJPvjMIY7FCCw/v/B+JzJbB/yuoioRzcU4vrisY9uRq54vVYcX7hH1DldsJFPTuXY3Id1WBflHA5I9Aw==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
lodash "^4.17.15"
"@abp/luxon@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-4.2.0-rc.2.tgz#ace30caacf6abcce1559b65046ea55e47e1cb8a2"
integrity sha512-ivhOadlaeBh6Pi3S5Bo0pb4EPCEjMFL5N5tZXGGieZt7EnF+4uiXbKJVErXNCDtX1jhJT/3HGqmdxa+WynUE0A==
"@abp/luxon@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-4.2.0.tgz#9cd02faa02cda1315f3e23ae204991ef47d6ae7c"
integrity sha512-dwtv2kqWCDyQADA1Os0aIy6Au2PhBtN6q9kukLWCvYmQZI23OKuEBMSngbJVTqEPfz0LV6CWbsWCUC3okAs46A==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
luxon "^1.24.1"
"@abp/malihu-custom-scrollbar-plugin@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-4.2.0-rc.2.tgz#483668c09e13b0ebcea16320a16f8517fcdd53ee"
integrity sha512-IYOwJcWyJkn/gdp4VkEYCv2Gi5+Boxe7pE7hA49gxgllHiFV0ABDFrRg4h8MbyJd80iwvVPo8t8ouvn4Snm+gQ==
"@abp/malihu-custom-scrollbar-plugin@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-4.2.0.tgz#2f8538df5567a2cf227998e4e2fb9a44b6e814eb"
integrity sha512-3D5REdR7yw2sRRfm3Oi+qlhDABLrKvfU/l7JcCJy+vrBvadx3e3pTdTLXsGptypPN3x/Qr400LgwIrrgyefN8g==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
malihu-custom-scrollbar-plugin "^3.1.5"
"@abp/prismjs@^4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-4.2.0-rc.2.tgz#6fe8f0063f4d674ddf309725acf65225ee3b1b1e"
integrity sha512-pvqGp5FmcDPVKBsPsPbJsbdLRK1i9RJGawYZUSwzyNOdjckf+jWviyFCN/xwt8gx5eHB2eY/VO957yX0y0ti2A==
"@abp/prismjs@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-4.2.0.tgz#8cedfac538d225930c3654f0d94cad0102faa2f4"
integrity sha512-f4duxGD47p2TDMNiiBcvvjOG1hjhSbAoXyq306RrbP0CBUVTdjzzVhCPPnr5+nMoG55VlRBqh92zkax5kdgwQg==
dependencies:
"@abp/clipboard" "~4.2.0-rc.2"
"@abp/core" "~4.2.0-rc.2"
"@abp/clipboard" "~4.2.0"
"@abp/core" "~4.2.0"
prismjs "^1.20.0"
"@abp/select2@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-4.2.0-rc.2.tgz#d11e808dac90e09e14025b6ba6dabed683a9b1fb"
integrity sha512-fENomkoAML+NQWvZ/ecjvqnXtvrzJKyChwj9Ul12c+Noi6boeuFDbhi/OG6qCrSzDnNBrehHVXBLFxT9sIL0OA==
"@abp/select2@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-4.2.0.tgz#eb05d22b1390c037911ba25e5ca0f2ed508a93b1"
integrity sha512-VlNoa9+F1/kGmaEI2wbL/cRNeAEpp0UdLpbadAnsmpUIODxCULCWS556Q4Y6Ff4CzYtzkYz2qaJ8T8pn+3EOcA==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
select2 "^4.0.13"
"@abp/sweetalert@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-4.2.0-rc.2.tgz#2359eeb9e4b34c4cbb3aaed11136ea3d280bd677"
integrity sha512-oQPj2vKPNAOvBECgJFdDwvJ8nwQ54ZPsAW1XS3XSkFz3YwWn4Xt9eZK6vecHloYckSckrFtsN/DSQI8ao/uIFg==
"@abp/sweetalert@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-4.2.0.tgz#b1daf641f27f0c8c2e246d364d6285c133faf773"
integrity sha512-AarV/L031xNB1gk/OYEUxkKZmls7zTwovS2t3ZrmwXXoav7nBCSPjEf1ByR2yhZRU/Yo1SNY/CNQQ4dgyf7Xng==
dependencies:
"@abp/core" "~4.2.0-rc.2"
"@abp/core" "~4.2.0"
sweetalert "^2.1.2"
"@abp/timeago@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-4.2.0-rc.2.tgz#d243acff635978068ec579c83a34d0680616c5ef"
integrity sha512-TF1dt9ro3BQnfVetDzbOkedSIncSwvPfnHKMY1xcLG2h0c6ke2PjDQ+4QJrtigggT4vfgmQtRrs/zJxjzWS7qw==
"@abp/timeago@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-4.2.0.tgz#9b15c496b7e63df93921d41de9e9eaf35a8dccbe"
integrity sha512-hyuLVGluHxtWEhR0VakPx5UCbyPcfq4lECEgHhI62PvH0xcSNNZ3bp1QnBTd3se/HAYvwA5sCwJY0YXQgUF2UQ==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
timeago "^1.6.7"
"@abp/toastr@~4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-4.2.0-rc.2.tgz#4e76f4b7068f92b267b725ad597b8315e6b615bc"
integrity sha512-UJVuQWc7py54JC35RrH+MwAZsOoOhejxX+3aonz8Io2rgFFj0YwK5U+vItRNM3iabSqTcIExcSD3XL2TmQyVzg==
"@abp/toastr@~4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-4.2.0.tgz#400e2f28b19c72b131ca32aa188ff5151d035752"
integrity sha512-tuGZd3kXqj1t/Y1xN8sOUH1KxiArYKD7xkhuajoOS65Ex5ad8mHPuzCtu8Pv+KL8TVFys9x3U3Tg24DQ6ASjhQ==
dependencies:
"@abp/jquery" "~4.2.0-rc.2"
"@abp/jquery" "~4.2.0"
toastr "^2.1.4"
"@abp/utils@^4.2.0-rc.2":
version "4.2.0-rc.2"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.2.0-rc.2.tgz#43c27d421da8e58a5d74c0a2df8742efa827c842"
integrity sha512-6gSLTP9s88aFjxm+fU2iZMzv5Eoe5/va2PHTAoQJA4BaP/SpFRGts3cT4HYWPfAbmzZVZ+tvjuYd3P6d3FCAhw==
"@abp/utils@^4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.2.0.tgz#073330e3e3f6ee61892f50260e48dbe1b05df35e"
integrity sha512-75qR3SdiAa75wqleAx9sUMXLj4m9duuBo5+2sVv7Y29GcEyKUPvm8B1s6tksvSGA0e3vnFTHeVEc10eD1xKHSQ==
dependencies:
just-compare "^1.3.0"

@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute.Extensions;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Content;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Http.Client;
using Volo.Abp.TestApp.Application;
@ -168,5 +171,31 @@ namespace Volo.Abp.Http.DynamicProxying
result.Inner1.Value2.ShouldBe("value two");
result.Inner1.Inner2.Value3.ShouldBe("value three");
}
[Fact]
public async Task DownloadAsync()
{
var result = await _peopleAppService.DownloadAsync();
result.ContentType.ShouldBe("application/rtf");
using (var reader = new StreamReader(result.GetStream()))
{
var str = await reader.ReadToEndAsync();
str.ShouldBe("DownloadAsync");
}
}
[Fact]
public async Task UploadAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("UploadAsync"));
memoryStream.Position = 0;
var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
});
result.ShouldBe("UploadAsync:application/rtf");
}
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Content;
using Volo.Abp.TestApp.Application.Dto;
namespace Volo.Abp.TestApp.Application
@ -20,5 +21,9 @@ namespace Volo.Abp.TestApp.Application
Task GetWithAuthorized();
Task<GetWithComplexTypeInput> GetWithComplexType(GetWithComplexTypeInput input);
Task<IRemoteStreamContent> DownloadAsync();
Task<string> UploadAsync(IRemoteStreamContent streamContent);
}
}

@ -1,12 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Application.Services;
using Volo.Abp.Content;
using Volo.Abp.TestApp.Application.Dto;
namespace Volo.Abp.TestApp.Application
@ -64,5 +67,24 @@ namespace Volo.Abp.TestApp.Application
{
return Task.FromResult(input);
}
public async Task<IRemoteStreamContent> DownloadAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync"));
return new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
};
}
public async Task<string> UploadAsync(IRemoteStreamContent streamContent)
{
using (var reader = new StreamReader(streamContent.GetStream()))
{
return await reader.ReadToEndAsync() + ":" + streamContent.ContentType;
}
}
}
}

@ -1,7 +1,9 @@
using Shouldly;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Modularity;
using Volo.Abp.TestApp.Domain;
@ -119,5 +121,40 @@ namespace Volo.Abp.TestApp.Testing
john.ShouldBeNull();
}
}
[Fact]
public async Task Should_HardDelete_WithDeleteMany()
{
var persons = await PersonRepository.GetListAsync();
await WithUnitOfWorkAsync(async () =>
{
var hardDeleteEntities = (HashSet<IEntity>)UnitOfWorkManager.Current.Items.GetOrAdd(
UnitOfWorkItemNames.HardDeletedEntities,
() => new HashSet<IEntity>()
);
hardDeleteEntities.UnionWith(persons);
await PersonRepository.DeleteManyAsync(persons);
});
var personsCount = await PersonRepository.GetCountAsync();
personsCount.ShouldBe(0);
}
[Fact]
public async Task Should_HardDelete_WithDeleteMany_WithPredicate()
{
await WithUnitOfWorkAsync(async () =>
{
await PersonRepository.HardDeleteAsync(x => x.Id == TestDataBuilder.UserDouglasId);
await PersonRepository.DeleteManyAsync(new[] { TestDataBuilder.UserDouglasId });
});
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldBeNull();
}
}
}

@ -44,9 +44,10 @@ namespace Volo.Abp.AuditLogging
Task<Dictionary<DateTime, double>> GetAverageExecutionDurationPerDayAsync(
DateTime startDate,
DateTime endDate);
DateTime endDate,
CancellationToken cancellationToken = default);
Task<EntityChange> GetEntityChange(Guid entityChangeId);
Task<EntityChange> GetEntityChange(Guid entityChangeId, CancellationToken cancellationToken = default);
Task<List<EntityChange>> GetEntityChangeListAsync(
string sorting = null,
@ -70,8 +71,8 @@ namespace Volo.Abp.AuditLogging
string entityTypeFullName = null,
CancellationToken cancellationToken = default);
Task<EntityChangeWithUsername> GetEntityChangeWithUsernameAsync(Guid entityChangeId);
Task<EntityChangeWithUsername> GetEntityChangeWithUsernameAsync(Guid entityChangeId, CancellationToken cancellationToken = default);
Task<List<EntityChangeWithUsername>> GetEntityChangesWithUsernameAsync(string entityId, string entityTypeFullName);
Task<List<EntityChangeWithUsername>> GetEntityChangesWithUsernameAsync(string entityId, string entityTypeFullName, CancellationToken cancellationToken = default);
}
}

@ -125,14 +125,17 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
.WhereIf(minExecutionDuration != null && minExecutionDuration.Value > 0, auditLog => auditLog.ExecutionDuration >= minExecutionDuration);
}
public virtual async Task<Dictionary<DateTime, double>> GetAverageExecutionDurationPerDayAsync(DateTime startDate, DateTime endDate)
public virtual async Task<Dictionary<DateTime, double>> GetAverageExecutionDurationPerDayAsync(
DateTime startDate,
DateTime endDate,
CancellationToken cancellationToken = default)
{
var result = await (await GetDbSetAsync()).AsNoTracking()
.Where(a => a.ExecutionTime < endDate.AddDays(1) && a.ExecutionTime > startDate)
.OrderBy(t => t.ExecutionTime)
.GroupBy(t => new { t.ExecutionTime.Date })
.Select(g => new { Day = g.Min(t => t.ExecutionTime), avgExecutionTime = g.Average(t => t.ExecutionDuration) })
.ToListAsync();
.ToListAsync(GetCancellationToken(cancellationToken));
return result.ToDictionary(element => element.Day.ClearTime(), element => element.avgExecutionTime);
}
@ -148,14 +151,16 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
return (await GetQueryableAsync()).IncludeDetails();
}
public virtual async Task<EntityChange> GetEntityChange(Guid entityChangeId)
public virtual async Task<EntityChange> GetEntityChange(
Guid entityChangeId,
CancellationToken cancellationToken = default)
{
var entityChange = await (await GetDbContextAsync()).Set<EntityChange>()
.AsNoTracking()
.IncludeDetails()
.Where(x => x.Id == entityChangeId)
.OrderBy(x => x.Id)
.FirstOrDefaultAsync();
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
if (entityChange == null)
{
@ -201,10 +206,12 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
return totalCount;
}
public virtual async Task<EntityChangeWithUsername> GetEntityChangeWithUsernameAsync(Guid entityChangeId)
public virtual async Task<EntityChangeWithUsername> GetEntityChangeWithUsernameAsync(
Guid entityChangeId,
CancellationToken cancellationToken = default)
{
var auditLog = await (await GetDbSetAsync()).AsNoTracking().IncludeDetails()
.Where(x => x.EntityChanges.Any(y => y.Id == entityChangeId)).FirstAsync();
.Where(x => x.EntityChanges.Any(y => y.Id == entityChangeId)).FirstAsync(GetCancellationToken(cancellationToken));
return new EntityChangeWithUsername()
{
@ -213,7 +220,10 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
};
}
public virtual async Task<List<EntityChangeWithUsername>> GetEntityChangesWithUsernameAsync(string entityId, string entityTypeFullName)
public virtual async Task<List<EntityChangeWithUsername>> GetEntityChangesWithUsernameAsync(
string entityId,
string entityTypeFullName,
CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync();
@ -225,7 +235,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
return await (from e in query
join auditLog in dbContext.AuditLogs on e.AuditLogId equals auditLog.Id
select new EntityChangeWithUsername {EntityChange = e, UserName = auditLog.UserName})
.OrderByDescending(x => x.EntityChange.ChangeTime).ToListAsync();
.OrderByDescending(x => x.EntityChange.ChangeTime).ToListAsync(GetCancellationToken(cancellationToken));
}
protected virtual async Task<IQueryable<EntityChange>> GetEntityChangeListQueryAsync(

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save