@ -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. |
|||
|
|||
 |
|||
|
|||
*(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.) |
|||
|
|||
 |
|||
|
|||
* 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. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
## 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. |
|||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -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. |
|||
|
|||
 |
|||
|
|||
<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: |
|||
|
|||
 |
|||
|
|||
## 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: |
|||
|
|||
 |
|||
|
|||
## 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: |
|||
|
|||
 |
|||
|
|||
|
|||
## 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. |
|||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 392 KiB |
@ -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) |
|||
|
|||
|
|||
@ -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: |
|||
|
|||
 |
|||
|
|||
The long text has been truncated by using the directive. |
|||
|
|||
The UI before using the directive looks like this: |
|||
|
|||
 |
|||
|
|||
### 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: |
|||
|
|||
 |
|||
@ -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 |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
## 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) {} |
|||
} |
|||
``` |
|||
|
|||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 46 KiB |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||