|
After Width: | Height: | Size: 9.9 KiB |
@ -1,3 +0,0 @@ |
|||
# Dashboards |
|||
|
|||
TODO |
|||
@ -0,0 +1,126 @@ |
|||
## Getting Started With the Angular Application Template |
|||
|
|||
This tutorial explain how to create a new Angular application using the startup template, configure and run it. |
|||
|
|||
### Creating a New Project |
|||
|
|||
This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. |
|||
|
|||
Install the ABP CLI using a command line window, if you've not installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Use `abp new` command in an empty folder to create your project: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -u angular |
|||
```` |
|||
|
|||
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. |
|||
|
|||
`-u angular` option specifies the UI framework to be Angular. Default database provider is EF Core. See the [CLI documentation](CLI.md) for all available options. |
|||
|
|||
#### Pre Requirements |
|||
|
|||
The created solution requires; |
|||
|
|||
* [Visual Studio 2017 (v15.9.0+)](https://visualstudio.microsoft.com/tr/downloads/) |
|||
* [.NET Core 2.2+](https://www.microsoft.com/net/download/dotnet-core/) |
|||
* [Node v10.16+](https://nodejs.org) |
|||
* [Yarn v1.17+](https://yarnpkg.com/) |
|||
|
|||
### The Solution Structure |
|||
|
|||
Open the solution in **Visual Studio**: |
|||
|
|||
 |
|||
|
|||
The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database. |
|||
|
|||
> See the [Application Template Document](Startup-Templates/Application.md) to understand the solution structure in details. |
|||
|
|||
### Database Connection String |
|||
|
|||
Check the **connection string** in the `appsettings.json` file under the `.HttpApi.Host` project: |
|||
|
|||
````json |
|||
{ |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need. |
|||
|
|||
### Create Database & Apply Database Migrations |
|||
|
|||
You have two options to create the database. |
|||
|
|||
#### Using the DbMigrator Application |
|||
|
|||
The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment. |
|||
|
|||
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. |
|||
|
|||
Right click to the `.DbMigrator` project and select **Set as StartUp Project**: |
|||
|
|||
 |
|||
|
|||
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: |
|||
|
|||
 |
|||
|
|||
#### Using EF Core Update-Database Command |
|||
|
|||
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.Web` project and select **Set as StartUp Project**: |
|||
|
|||
 |
|||
|
|||
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: |
|||
|
|||
 |
|||
|
|||
This will create a new database based on the configured connection string. |
|||
|
|||
> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application. |
|||
|
|||
### Running the Application |
|||
|
|||
#### Run the API Host (Server Side) |
|||
|
|||
Ensure that the `.HttpApi.Host` project is the startup project and un the application which will open a Swagger UI: |
|||
|
|||
 |
|||
|
|||
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. |
|||
|
|||
##### Authorization for the Swagger UI |
|||
|
|||
Most of the application APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. |
|||
|
|||
#### Run the Angular Application (Client Side) |
|||
|
|||
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com) package manager while npm install will also work in most cases): |
|||
|
|||
````bash |
|||
yarn |
|||
```` |
|||
|
|||
Once all node modules are loaded, execute `yarn start` or `npm start` command: |
|||
|
|||
````bash |
|||
yarn start |
|||
```` |
|||
|
|||
Open your favorite browser and go to `localhost:4200` URL. Initial username is `admin` and password is `1q2w3E*`. |
|||
|
|||
The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. |
|||
|
|||
> We recommend [Visual Studio Code](https://code.visualstudio.com/) as the editor for the Angular project, but you are free to use your favorite editor. |
|||
|
|||
### What's Next? |
|||
|
|||
* [Application development tutorial](Tutorials/Angular/Part-I.md) |
|||
@ -0,0 +1,274 @@ |
|||
# Application Startup Template |
|||
|
|||
## Introduction |
|||
|
|||
This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below: |
|||
|
|||
* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider). |
|||
* See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider). |
|||
|
|||
## How to Start With? |
|||
|
|||
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. |
|||
|
|||
First, install the ABP CLI if you haven't installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -t app |
|||
```` |
|||
|
|||
* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
* This example specified the template name (`-t` or `--template` option). However, `app` is already the default template if you don't specify it. |
|||
|
|||
### Specify the UI Framework |
|||
|
|||
This template provides multiple UI frameworks: |
|||
|
|||
* `mvc`: ASP.NET Core MVC UI with Razor Pages (default) |
|||
* `angular`: Angular UI |
|||
|
|||
Use `-u` or `--ui` option to specify the UI framework: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -u angular |
|||
```` |
|||
|
|||
### Specify the Database Provider |
|||
|
|||
This template supports the following database providers: |
|||
|
|||
- `ef`: Entity Framework Core (default) |
|||
- `mongodb`: MongoDB |
|||
|
|||
Use `-d` (or `--database-provider`) option to specify the database provider: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -d mongodb |
|||
```` |
|||
|
|||
## Solution Structure |
|||
|
|||
Based on the options you've specified, you will get a slightly different solution structure. |
|||
|
|||
### Default Structure |
|||
|
|||
If you don't specify any additional option, you will have a solution like shown below: |
|||
|
|||
 |
|||
|
|||
Projects are organized in `src` and `test` folders. `src` folder contains the actual application which is layered based on [DDD](../Domain-Driven-Design.md) principles as mentioned before. |
|||
|
|||
The diagram below shows the layers & project dependencies of the application: |
|||
|
|||
 |
|||
|
|||
Each section below will explain the related project & its dependencies. |
|||
|
|||
#### .Domain.Shared Project |
|||
|
|||
This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. |
|||
|
|||
A `BookType` enum and a `BookConsts` class (which may have some constant fields for the `Book` entity, like `MaxNameLength`) are good candidates for this project. |
|||
|
|||
* This project has no dependency to other projects in the solution. All other projects depend on this directly or indirectly. |
|||
|
|||
#### .Domain Project |
|||
|
|||
This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. |
|||
|
|||
A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. |
|||
|
|||
* Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. |
|||
|
|||
#### .Application.Contracts Project |
|||
|
|||
This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It does exists to separate interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. |
|||
|
|||
An `IBookAppService` interface and a `BookCreationDto` class are good candidates for this project. |
|||
|
|||
* Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. |
|||
|
|||
#### .Application Project |
|||
|
|||
This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. |
|||
|
|||
A `BookAppService` class is a good candidate for this project. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. |
|||
* Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. |
|||
|
|||
#### .EntityFrameworkCore Project |
|||
|
|||
This is the integration project for the EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. |
|||
|
|||
* Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> This project is available only if you are using EF Core as the database provider. If you select another database provider, its name will be different. |
|||
|
|||
#### .EntityFrameworkCore.DbMigrations Project |
|||
|
|||
Contains EF Core database migrations for the solution. It has a separated `DbContext` to dedicated to manage migrations. |
|||
|
|||
ABP is a modular framework and with an ideal design, each module has its own `DbContext` class. This is where the migration `DbContext` comes into play and unifies all `DbContext` configurations into a single model to maintain a single database schema. For more advanced scenarios, you can have multiple databases (each contains a single or a few module tables) and multiple migration `DbContext`s (each maintains a different database schema). |
|||
|
|||
Notice that the migration `DbContext` is only used for database migrations and *not used on runtime*. |
|||
|
|||
* Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application. |
|||
|
|||
> This project is available only if you are using EF Core as the database provider. |
|||
|
|||
#### .DbMigrator Project |
|||
|
|||
This is a console application which simplifies to execute database migrations on development and production environments. When you run this application, it; |
|||
|
|||
* Creates the database if necessary. |
|||
* Applies the pending database migrations. |
|||
* Seeds initial data if needed. |
|||
|
|||
> This project has its own `appsettings.json` file. So, if you want to change the database connection string, remember to change this file too. |
|||
|
|||
Especially, seeding initial data is important at this point. ABP has a modular data seed infrastructure. See [its documentation](../Data-Seeding.md) for more about the data seeding. |
|||
|
|||
While creating database & applying migrations seems only necessary for relational databases, this projects comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds initial data which is necessary for the application. |
|||
|
|||
* Depends on the `.EntityFrameworkCore.DbMigrations` project (for EF Core) since it needs to access to the migrations. |
|||
* Depends on the `.Application.Contracts` project to be able to access permission definitions, because initial data seeder grants all permissions for the admin role by default. |
|||
|
|||
#### .HttpApi Project |
|||
|
|||
This project is used to define your API Controllers. |
|||
|
|||
Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../AspNetCore/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. |
|||
|
|||
#### .HttpApi.Client Project |
|||
|
|||
This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other type of applications, they can still use your APIs, either manually or using a tool in their own platform) |
|||
|
|||
Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature. |
|||
|
|||
`.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to share the same application service interfaces and DTOs with the remote service. |
|||
|
|||
> You can delete this project & dependencies if you don't need to create C# client proxies for your APIs. |
|||
|
|||
#### .Web Project |
|||
|
|||
This project contains the User Interface (UI) of the application if you are using ASP.NET Core MVC UI. It contains Razor pages, JavaScript files, CSS files, images and so on... |
|||
|
|||
This project contains the main `appsettings.json` file that contains the connection string and other configuration of the application. |
|||
|
|||
* Depends on the `.HttpApi` since UI layer needs to use APIs and application service interfaces of the solution. |
|||
|
|||
> If you check the source code of the `.Web.csproj` file, you will see the references to the `.Application` and the `.EntityFrameworkCore.DbMigrations` projects. |
|||
> |
|||
> These references are actually not needed while coding your UI layer, because UI layer normally doesn't depend on the EF Core or the Application layer's implementation. This startup templates are ready for the tiered deployment, where API layer is hosted in a separate server than the UI layer. |
|||
> |
|||
> However, if you don't choose the `--tiered` option, these references will be in the .Web project to be able to host the Web, API and application layers in a single application endpoint. |
|||
> |
|||
> This gives you to ability to use domain entities & repositories in your presentation layer. However, this is considered as a bad practice according to the DDD. |
|||
|
|||
#### Test Projects |
|||
|
|||
The solution has multiple test projects, one for each layer: |
|||
|
|||
* `.Domain.Tests` is used to test the domain layer. |
|||
* `.Application.Tests` is used to test the application layer. |
|||
* `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. |
|||
* `.Web.Tests` is used to test the UI (if you are using ASP.NET Core MVC UI). |
|||
* `.TestBase` is a base (shared) project for all tests. |
|||
|
|||
In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a .NET application. |
|||
|
|||
Test projects are prepared for integration testing; |
|||
|
|||
* It is fully integrated to ABP framework and all services in your application. |
|||
* It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library. |
|||
* Authorization is disabled, so any application service can be easily used in tests. |
|||
|
|||
You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all initialization process). |
|||
|
|||
#### How to Run? |
|||
|
|||
Set `.Web` as the startup project and run the application. Default username is `admin` and password is `1q2w3E*`. |
|||
|
|||
See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) for more information. |
|||
|
|||
### Tiered Structure |
|||
|
|||
If you have selected the ASP.NET Core UI and specified the `--tiered` option, the solution created will be a tiered solution. The purpose of the tiered structure is to be able to **deploy Web application and HTTP API to different servers**: |
|||
|
|||
 |
|||
|
|||
* Browser runs your UI by executing HTML, CSS & JavaScript. |
|||
* Web servers hosts static UI files (CSS, JavaScript, image... etc.) & dynamic components (e.g. Razor pages). It performs HTTP requests to the API server to execute the business logic of the application. |
|||
* API Server hosts the HTTP APIs which then use application & domain layers of the application to perform the business logic. |
|||
* Finally, database server hosts your database. |
|||
|
|||
So, the resulting solution allows a 4-tiered deployment, by comparing to 3-tiered deployment of the default structure explained before. |
|||
|
|||
> Unless you actually need to such a 4-tiered deployment, its suggested to go with the default structure which is simpler to develop, deploy and maintain. |
|||
|
|||
The solution structure is shown below: |
|||
|
|||
 |
|||
|
|||
As different from the default structure, two new projects come into play: `.IdentityServer` & `.HttpApi.Host`. |
|||
|
|||
#### .IdentityServer Project |
|||
|
|||
This project is used as an authentication server for other projects. `.Web` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the IdentityServer. Then uses the access token to call the HTTP API server. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. |
|||
|
|||
 |
|||
|
|||
ABP uses the open source [IdentityServer4](https://identityserver.io/) framework for the authentication between applications. See [IdentityServer4 documentation](http://docs.identityserver.io) for details about the IdentityServer4 and OpenID Connect protocol. |
|||
|
|||
It has its own `appsettings.json` that contains database connection and other configurations. |
|||
|
|||
#### .HttpApi.Host Project |
|||
|
|||
This project is an application that hosts the API of the solution. It has its own `appsettings.json` that contains database connection and other configurations. |
|||
|
|||
#### .Web Project |
|||
|
|||
Just like the default structure, this project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... |
|||
|
|||
This project contains an `appsettings.json` file, but this time it does not have a connection string because it never connects to the database. Instead, it mainly contains endpoint of the remote API server and the authentication server. |
|||
|
|||
#### Pre-requirements |
|||
|
|||
* [Redis](https://redis.io/): The applications use Redis as as distributed cache. So, you need to have Redis installed & running. |
|||
|
|||
#### How to Run? |
|||
|
|||
You should run the application with the given order: |
|||
|
|||
* First, run the `.IdentityServer` since other applications depends on it. |
|||
* Then run the `.HttpApi.Host` since it is used by the `.Web` application. |
|||
* Finally, you can run the `.Web` project and login to the application (using `admin` as the username and `1q2w3E*` as the password). |
|||
|
|||
### Angular UI |
|||
|
|||
If you choose Angular as the UI framework (using the `-u angular` option), the solution is separated into two folders: |
|||
|
|||
* `angular` folder contains the Angular UI solution, the client side. |
|||
* `aspnet-core` folder contains the ASP.NET Core solution, the server side. |
|||
|
|||
Server side is very similar to the solution described above. `.HttpApi.Host` project serves the API, so the Angular application can consume it. |
|||
|
|||
The files under the `angular/src/environments` folder has the essential configuration of the application. |
|||
|
|||
#### |
|||
|
|||
## What's Next? |
|||
|
|||
- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. |
|||
- See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template. |
|||
@ -1,11 +1,9 @@ |
|||
# Startup Templates |
|||
|
|||
While you can start with an empty project and add needed packages manually, startup templates makes easy and comfortable to start a new solution with the ABP framework. |
|||
While you can start with an empty project and add needed packages manually, startup templates make easy and comfortable to start a new solution with the ABP framework. Click the name from the list below to see the documentation of the related startup template: |
|||
|
|||
Click to the name from the list below to see the documentation of the related startup template: |
|||
|
|||
* [**mvc**](Mvc.md): ASP.NET Core MVC application template. |
|||
* [**mvc-module**](Mvc-Module.md): ASP.NET Core MVC module/service template. |
|||
* [**app**](Application.md): Application template. |
|||
* [**module**](Module.md): Module/service template. |
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,158 @@ |
|||
# MVC Module Startup Template |
|||
|
|||
This template can be used to create a **reusable [application module](../Modules/Index.md)** based on the [module development best practices & conventions](../Best-Practices/Index.md). It is also suitable for creating **microservices** (with or without UI). |
|||
|
|||
## How to Start With? |
|||
|
|||
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. |
|||
|
|||
First, install the ABP CLI if you haven't installed before: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
```bash |
|||
abp new Acme.IssueManagement -t module |
|||
``` |
|||
|
|||
- `Acme.IssueManagement` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
|
|||
### Without User Interface |
|||
|
|||
The template comes with an MVC UI by default. You can use `--no-ui` option to not include the UI layer. |
|||
|
|||
````bash |
|||
abp new Acme.IssueManagement -t mvc-module --no-ui |
|||
```` |
|||
|
|||
## Solution Structure |
|||
|
|||
Based on the options you've specified, you will get a slightly different solution structure. If you don't specify any option, you will have a solution like shown below: |
|||
|
|||
 |
|||
|
|||
Projects are organized as `src`, `test` and `host` folders: |
|||
|
|||
* `src` folder contains the actual module which is layered based on [DDD](../Domain-Driven-Design.md) principles. |
|||
* `test` folder contains unit & integration tests. |
|||
* `host` folder contains applications with different configurations to demonstrate how to host the module in an application. These are not a part of the module, but useful on development. |
|||
|
|||
The diagram below shows the layers & project dependencies of the module: |
|||
|
|||
 |
|||
|
|||
Each section below will explain the related project & its dependencies. |
|||
|
|||
### .Domain.Shared Project |
|||
|
|||
This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. |
|||
|
|||
An `IssueType` enum and an `IssueConsts` class (which may have some constant fields for the `Issue` entity, like `MaxTitleLength`) are good candidates for this project. |
|||
|
|||
- This project has no dependency to other projects in the solution. All other projects depend on this directly or indirectly. |
|||
|
|||
### .Domain Project |
|||
|
|||
This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. |
|||
|
|||
An `Issue` entity, an `IssueManager` domain service and an `IIssueRepository` interface are good candidates for this project. |
|||
|
|||
- Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. |
|||
|
|||
### .Application.Contracts Project |
|||
|
|||
This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It does exists to separate interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. |
|||
|
|||
An `IIssueAppService` interface and an `IssueCreationDto` class are good candidates for this project. |
|||
|
|||
- Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. |
|||
|
|||
### .Application Project |
|||
|
|||
This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. |
|||
|
|||
An `IssueAppService` class is a good candidate for this project. |
|||
|
|||
- Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. |
|||
- Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. |
|||
|
|||
### .EntityFrameworkCore Project |
|||
|
|||
This is the integration project for EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. |
|||
|
|||
- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> You can delete this project if you don't want to support EF Core for your module. |
|||
|
|||
### .MongoDB Project |
|||
|
|||
This is the integration project for MongoDB. |
|||
|
|||
- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> You can delete this project if you don't want to support MongoDB for your module. |
|||
|
|||
### Test Projects |
|||
|
|||
The solution has multiple test projects, one for each layer: |
|||
|
|||
- `.Domain.Tests` is used to test the domain layer. |
|||
- `.Application.Tests` is used to test the application layer. |
|||
- `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. |
|||
- `.MongoDB.Tests` is used to test MongoDB configuration and custom repositories. |
|||
- `.TestBase` is a base (shared) project for all tests. |
|||
|
|||
In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a Dotnet application. |
|||
|
|||
Test projects are prepared for integration testing; |
|||
|
|||
- It is fully integrated to ABP framework and all services in your application. |
|||
- It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library. |
|||
- Authorization is disabled, so any application service can be easily used in tests. |
|||
|
|||
You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all initialization process). |
|||
|
|||
> Domain & Application tests are using EF Core. If you remove EF Core integration or you want to use MongoDB for testing these layers, you should manually change project references & module dependencies. |
|||
|
|||
### Host Projects |
|||
|
|||
The solution has a few host applications to run your module. Host applications are used to run your module in a fully configured application. It is useful on development. Host applications includes some other modules in addition to the module being developed: |
|||
|
|||
Host applications support two types of scenarios. |
|||
|
|||
#### Single (Unified) Application Scenario |
|||
|
|||
If your module has a UI, then `.Web.Unified` application is used to host the UI and API on a single point. It has its own `appsettings.json` file (that includes the database connection string) and EF Core database migrations. |
|||
|
|||
For the `.Web.Unified` application, there is a single database, named `YourProjectName_Unified` (like *IssueManagement_Unified* for this sample). |
|||
|
|||
> If you've selected the `--no-ui` option, this project will not be in your solution. |
|||
|
|||
##### How to Run? |
|||
|
|||
Set it as the startup project, run `Update-Database` command for the EF Core from Package Manager Console and run your application. Default username is `admin` and password is `1q2w3E*`. |
|||
|
|||
#### Separated Deployment & Databases Scenario |
|||
|
|||
In this scenario, there are three applications; |
|||
|
|||
* `.IdentityServer` application is an authentication server used by other applications. It has its own `appsettings.json` that contains database connection and other configurations. |
|||
* `.HttpApi.Host` hosts the HTTP API of the module. It has its own `appsettings.json` that contains database connections and other configurations. |
|||
* `.Web.Host` host the UI of the module. This project contains an `appsettings.json` file, but it does not have a connection string because it never connects to the database. Instead, it mainly contains endpoint of the remote API server and the authentication server. |
|||
|
|||
The diagram below shows the relation of the applications: |
|||
|
|||
 |
|||
|
|||
`.Web.Host` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the `.IdentityServer`. Then uses the access token to call the `.HttpApi.Host`. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. |
|||
|
|||
##### How to Run? |
|||
|
|||
You should run the application with the given order: |
|||
|
|||
- First, run the `.IdentityServer` since other applications depends on it. |
|||
- Then run the `.HttpApi.Host` since it is used by the `.Web.Host` application. |
|||
- Finally, you can run the `.Web.Host` project and login to the application using `admin` as the username and `1q2w3E*` as the password. |
|||
@ -1,158 +0,0 @@ |
|||
# MVC Module Startup Template |
|||
|
|||
This template can be used to create a **reusable [application module](../Modules/Index.md)** based on the [module development best practices & conventions](../Best-Practices/Index.md). It is also suitable for creating **microservices** (with or without UI). |
|||
|
|||
## How to Start With? |
|||
|
|||
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. |
|||
|
|||
First, install the ABP CLI if you haven't installed before: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
```bash |
|||
abp new Acme.IssueManagement -t mvc-module |
|||
``` |
|||
|
|||
- `Acme.IssueManagement` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
|
|||
### Without User Interface |
|||
|
|||
The template comes with a UI by default. You can use `--no-ui` option to not include the UI layer. |
|||
|
|||
````bash |
|||
abp new Acme.IssueManagement -t mvc-module --no-ui |
|||
```` |
|||
|
|||
## Solution Structure |
|||
|
|||
Based on the options you've specified, you will get a slightly different solution structure. If you don't specify any option, you will have a solution like shown below: |
|||
|
|||
 |
|||
|
|||
Projects are organized as `src`, `test` and `host` folders: |
|||
|
|||
* `src` folder contains the actual module which is layered based on [DDD](../Domain-Driven-Design.md) principles. |
|||
* `test` folder contains unit & integration tests. |
|||
* `host` folder contains applications with different configurations to demonstrate how to host the module in an application. These are not a part of the module, but useful on development. |
|||
|
|||
The diagram below shows the layers & project dependencies of the module: |
|||
|
|||
 |
|||
|
|||
Each section below will explain the related project & its dependencies. |
|||
|
|||
### .Domain.Shared Project |
|||
|
|||
This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. |
|||
|
|||
An `IssueType` enum and an `IssueConsts` class (which may have some constant fields for the `Issue` entity, like `MaxTitleLength`) are good candidates for this project. |
|||
|
|||
- This project has no dependency to other projects in the solution. All other projects depend on this directly or indirectly. |
|||
|
|||
### .Domain Project |
|||
|
|||
This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. |
|||
|
|||
An `Issue` entity, an `IssueManager` domain service and an `IIssueRepository` interface are good candidates for this project. |
|||
|
|||
- Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. |
|||
|
|||
### .Application.Contracts Project |
|||
|
|||
This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It does exists to separate interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. |
|||
|
|||
An `IIssueAppService` interface and an `IssueCreationDto` class are good candidates for this project. |
|||
|
|||
- Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. |
|||
|
|||
### .Application Project |
|||
|
|||
This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. |
|||
|
|||
An `IssueAppService` class is a good candidate for this project. |
|||
|
|||
- Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. |
|||
- Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. |
|||
|
|||
### .EntityFrameworkCore Project |
|||
|
|||
This is the integration project for EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. |
|||
|
|||
- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> You can delete this project if you don't want to support EF Core for your module. |
|||
|
|||
### .MongoDB Project |
|||
|
|||
This is the integration project for MongoDB. |
|||
|
|||
- Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> You can delete this project if you don't want to support MongoDB for your module. |
|||
|
|||
### Test Projects |
|||
|
|||
The solution has multiple test projects, one for each layer: |
|||
|
|||
- `.Domain.Tests` is used to test the domain layer. |
|||
- `.Application.Tests` is used to test the application layer. |
|||
- `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. |
|||
- `.MongoDB.Tests` is used to test MongoDB configuration and custom repositories. |
|||
- `.TestBase` is a base (shared) project for all tests. |
|||
|
|||
In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a Dotnet application. |
|||
|
|||
Test projects are prepared for integration testing; |
|||
|
|||
- It is fully integrated to ABP framework and all services in your application. |
|||
- It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library. |
|||
- Authorization is disabled, so any application service can be easily used in tests. |
|||
|
|||
You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all initialization process). |
|||
|
|||
> Domain & Application tests are using EF Core. If you remove EF Core integration or you want to use MongoDB for testing these layers, you should manually change project references & module dependencies. |
|||
|
|||
### Host Projects |
|||
|
|||
The solution has a few host applications to run your module on development. Host applications are used to run your module in a fully configured application. It is useful on development. Host applications includes some other modules in addition to the module being developed: |
|||
|
|||
Host applications support two types of scenarios. |
|||
|
|||
#### Single (Unified) Application Scenario |
|||
|
|||
If your module has a UI, then `.Web.Unified` application is used to host the UI and API on a single point. It has its own `appsettings.json` file (that includes the database connection string) and EF Core database migrations. |
|||
|
|||
For the `.Web.Unified` application, there is a single database, named `YourProjectName_Unified` (like *IssueManagement_Unified* for this sample). |
|||
|
|||
> If you've selected the `--no-ui` option, this project will not be in your solution. |
|||
|
|||
##### How to Run? |
|||
|
|||
Set it as the startup project, run `Update-Database` command for the EF Core from Package Manager Console and run your application. Default username is `admin` and password is `1q2w3E*`. |
|||
|
|||
#### Separated Deployment & Databases Scenario |
|||
|
|||
In this scenario, there are three applications; |
|||
|
|||
* `.IdentityServer` application is an authentication server used by other applications. It has its own `appsettings.json` that contains database connection and other configurations. |
|||
* `.HttpApi.Host` hosts the HTTP API of the module. It has its own `appsettings.json` that contains database connections and other configurations. |
|||
* `.Web.Host` host the UI of the module. This project contains an `appsettings.json` file, but it does not have a connection string because it never connects to the database. Instead, it mainly contains endpoint of the remote API server and the authentication server. |
|||
|
|||
The diagram below shows the relation of the applications: |
|||
|
|||
 |
|||
|
|||
`.Web.Host` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the `.IdentityServer`. Then uses the access token to call the `.HttpApi.Host`. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. |
|||
|
|||
##### How to Run? |
|||
|
|||
You should run the application with the given order: |
|||
|
|||
- First, run the `.IdentityServer` since other applications depends on it. |
|||
- Then run the `.HttpApi.Host` since it is used by the `.Web.Host` application. |
|||
- Finally, you can run the `.Web.Host` project and login to the application using `admin` as the username and `1q2w3E*` as the password. |
|||
@ -1,260 +0,0 @@ |
|||
# MVC Application Startup Template |
|||
|
|||
## Introduction |
|||
|
|||
This template provides a layered (or tiered, based on the preference) application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. |
|||
|
|||
This document explains the solution structure and projects in details. |
|||
|
|||
* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. |
|||
* See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template. |
|||
|
|||
## How to Start With? |
|||
|
|||
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. |
|||
|
|||
First, install the ABP CLI if you haven't installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -t mvc |
|||
```` |
|||
|
|||
* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
* This example specified the template name (`-t` or `--template` option). However, `mvc` is already the default template if you don't specify it. |
|||
|
|||
### Specify the Database Provider |
|||
|
|||
This template supports the following database providers: |
|||
|
|||
- `ef`: Entity Framework Core (default) |
|||
- `mongodb`: MongoDB |
|||
|
|||
Use `-d` (or `--database-provider`) option to specify the database provider: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore -d mongodb |
|||
```` |
|||
|
|||
### Create a Tiered Solution |
|||
|
|||
`--tiered` option is used to create a tiered solution where Web and Http API layers are physically separated. If not specified, it creates a layered solution which is less complex and suitable for most scenarios. |
|||
|
|||
````bash |
|||
abp new Acme.BookStore --tiered |
|||
```` |
|||
|
|||
See the "Tiered Structure" section below for the tiered approach. |
|||
|
|||
## Solution Structure |
|||
|
|||
Based on the options you've specified, you will get a slightly different solution structure. |
|||
|
|||
### Default Structure |
|||
|
|||
If you don't specify any option, you will have a solution like shown below: |
|||
|
|||
 |
|||
|
|||
Projects are organized in `src` and `test` folders. `src` folder contains the actual application which is layered based on [DDD](../Domain-Driven-Design.md) principles as mentioned before. |
|||
|
|||
The diagram below shows the layers & project dependencies of the application: |
|||
|
|||
 |
|||
|
|||
Each section below will explain the related project & its dependencies. |
|||
|
|||
#### .Domain.Shared Project |
|||
|
|||
This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. |
|||
|
|||
A `BookType` enum and a `BookConsts` class (which may have some constant fields for the `Book` entity, like `MaxNameLength`) are good candidates for this project. |
|||
|
|||
* This project has no dependency to other projects in the solution. All other projects depend on this directly or indirectly. |
|||
|
|||
#### .Domain Project |
|||
|
|||
This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. |
|||
|
|||
A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. |
|||
|
|||
* Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. |
|||
|
|||
#### .Application.Contracts Project |
|||
|
|||
This project mainly contains [application service](../Application-Services.md) **interfaces** and [Data Transfer Objects](../Data-Transfer-Objects.md) (DTO) of the application layer. It does exists to separate interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. |
|||
|
|||
An `IBookAppService` interface and a `BookCreationDto` class are good candidates for this project. |
|||
|
|||
* Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. |
|||
|
|||
#### .Application Project |
|||
|
|||
This project contains the [application service](../Application-Services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. |
|||
|
|||
A `BookAppService` class is a good candidate for this project. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. |
|||
* Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. |
|||
|
|||
#### .EntityFrameworkCore Project |
|||
|
|||
This is the integration project for the EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. |
|||
|
|||
* Depends on the `.Domain` project to be able to reference to entities and repository interfaces. |
|||
|
|||
> This project is available only if you are using EF Core as the database provider. If you select another database provider, its name will be different. |
|||
|
|||
#### .EntityFrameworkCore.DbMigrations Project |
|||
|
|||
Contains EF Core database migrations for the solution. It has a separated `DbContext` to dedicated to manage migrations. |
|||
|
|||
ABP is a modular framework and with an ideal design, each module has its own `DbContext` class. This is where the migration `DbContext` comes into play and unifies all `DbContext` configurations into a single model to maintain a single database schema. For more advanced scenarios, you can have multiple databases (each contains a single or a few module tables) and multiple migration `DbContext`s (each maintains a different database schema). |
|||
|
|||
Notice that the migration `DbContext` is only used for database migrations and *not used on runtime*. |
|||
|
|||
* Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application. |
|||
|
|||
> This project is available only if you are using EF Core as the database provider. |
|||
|
|||
#### .DbMigrator Project |
|||
|
|||
This is a console application which simplifies to execute database migrations on development and production environments. When you run this application, it; |
|||
|
|||
* Creates the database if necessary. |
|||
* Applies the pending database migrations. |
|||
* Seeds initial data if needed. |
|||
|
|||
> This project has its own `appsettings.json` file. So, if you want to change the database connection string, remember to change this file too. |
|||
|
|||
Especially, seeding initial data is important at this point. ABP has a modular data seed infrastructure. See [its documentation](../Data-Seeding.md) for more about the data seeding. |
|||
|
|||
While creating database & applying migrations seems only necessary for relational databases, this projects comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds initial data which is necessary for the application. |
|||
|
|||
* Depends on the `.EntityFrameworkCore.DbMigrations` project (for EF Core) since it needs to access to the migrations. |
|||
* Depends on the `.Application.Contracts` project to be able to access permission definitions, because initial data seeder grants all permissions for the admin role by default. |
|||
|
|||
#### .HttpApi Project |
|||
|
|||
This project is used to define your API Controllers. |
|||
|
|||
Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../AspNetCore/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. |
|||
|
|||
#### .HttpApi.Client Project |
|||
|
|||
This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other type of applications, they can still use your APIs, either manually or using a tool in their own platform) |
|||
|
|||
Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature. |
|||
|
|||
`.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. |
|||
|
|||
* Depends on the `.Application.Contracts` project to be able to share the same application service interfaces and DTOs with the remote service. |
|||
|
|||
> You can delete this project & dependencies if you don't need to create C# client proxies for your APIs. |
|||
|
|||
#### .Web Project |
|||
|
|||
This project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... |
|||
|
|||
This project contains the main `appsettings.json` file that contains the connection string and other configuration of the application. |
|||
|
|||
* Depends on the `.HttpApi` since UI layer needs to use APIs and application service interfaces of the solution. |
|||
|
|||
> If you check the source code of the `.Web.csproj` file, you will see the references to the `.Application` and the `.EntityFrameworkCore.DbMigrations` projects. |
|||
> |
|||
> These references are actually not needed while coding your UI layer, because UI layer normally doesn't depend on the EF Core or the Application layer's implementation. This startup templates are ready for the tiered deployment, where API layer is hosted in a separate server than the UI layer. |
|||
> |
|||
> However, if you don't choose the `--tiered` option, these references will be in the .Web project to be able to host the Web, API and application layers in a single application endpoint. |
|||
> |
|||
> This gives you to ability to use domain entities & repositories in your presentation layer. However, this is considered as a bad practice according to the DDD. |
|||
|
|||
#### Test Projects |
|||
|
|||
The solution has multiple test projects, one for each layer: |
|||
|
|||
* `.Domain.Tests` is used to test the domain layer. |
|||
* `.Application.Tests` is used to test the application layer. |
|||
* `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. |
|||
* `.Web.Tests` is used to test the UI. |
|||
* `.TestBase` is a base (shared) project for all tests. |
|||
|
|||
In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a Dotnet application. |
|||
|
|||
Test projects are prepared for integration testing; |
|||
|
|||
* It is fully integrated to ABP framework and all services in your application. |
|||
* It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library. |
|||
* Authorization is disabled, so any application service can be easily used in tests. |
|||
|
|||
You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all initialization process). |
|||
|
|||
#### How to Run? |
|||
|
|||
Set `.Web` as the startup project and run the application. Default username is `admin` and password is `1q2w3E*`. |
|||
|
|||
See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) for more information. |
|||
|
|||
### Tiered Structure |
|||
|
|||
If you specify the `--tiered` option as described above, the solution created will be a tiered solution. The purpose of the tiered structure is to be able to **deploy Web application and HTTP API to different servers**: |
|||
|
|||
 |
|||
|
|||
* Browser runs your UI by executing HTML, CSS & JavaScript. |
|||
* Web servers hosts static UI files (CSS, JavaScript, image... etc.) & dynamic components (e.g. Razor pages). It performs HTTP requests to the API server to execute the business logic of the application. |
|||
* API Server hosts the HTTP APIs which then use application & domain layers of the application to perform the business logic. |
|||
* Finally, database server hosts your database. |
|||
|
|||
So, the resulting solution allows a 4-tiered deployment, by comparing to 3-tiered deployment of the default structure explained before. |
|||
|
|||
> Unless you actually need to such a 4-tiered deployment, its suggested to go with the default structure which is simpler to develop, deploy and maintain. |
|||
|
|||
The solution structure is shown below: |
|||
|
|||
 |
|||
|
|||
As different from the default structure, two new projects come into play: `.IdentityServer` & `.HttpApi.Host`. |
|||
|
|||
#### .IdentityServer Project |
|||
|
|||
This project is used as an authentication server for other projects. `.Web` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the IdentityServer. Then uses the access token to call the HTTP API server. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. |
|||
|
|||
 |
|||
|
|||
ABP uses the open source [IdentityServer4](https://identityserver.io/) framework for the authentication between applications. See [IdentityServer4 documentation](http://docs.identityserver.io) for details about the IdentityServer4 and OpenID Connect protocol. |
|||
|
|||
It has its own `appsettings.json` that contains database connection and other configurations. |
|||
|
|||
#### .HttpApi.Host Project |
|||
|
|||
This project is an application that hosts the API of the solution. It has its own `appsettings.json` that contains database connection and other configurations. |
|||
|
|||
#### .Web Project |
|||
|
|||
Just like the default structure, this project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... |
|||
|
|||
This project contains an `appsettings.json` file, but this time it does not have a connection string because it never connects to the database. Instead, it mainly contains endpoint of the remote API server and the authentication server. |
|||
|
|||
#### Pre-requirements |
|||
|
|||
* [Redis](https://redis.io/): The applications use Redis as as distributed cache. So, you need to have Redis installed & running. |
|||
|
|||
#### How to Run? |
|||
|
|||
You should run the application with the given order: |
|||
|
|||
* First, run the `.IdentityServer` since other applications depends on it. |
|||
* Then run the `.HttpApi.Host` since it is used by the `.Web` application. |
|||
* Finally, you can run the `.Web` project and login to the application (using `admin` as the username and `1q2w3E*` as the password). |
|||
|
|||
## What's Next? |
|||
|
|||
- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. |
|||
- See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template. |
|||
@ -0,0 +1,660 @@ |
|||
## Angular Tutorial - Part I |
|||
|
|||
### About this Tutorial |
|||
|
|||
In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Angular** will be used as the UI framework and **MongoDB** will be used as the database provider. |
|||
|
|||
This is the first part of the Angular tutorial series. See all parts: |
|||
|
|||
- **Part I: Create the project and a book list page (this tutorial)** |
|||
- [Part II: Create, Update and Delete books](Part-II.md) |
|||
|
|||
You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). |
|||
|
|||
### Creating the Project |
|||
|
|||
Create a new project named `Acme.BookStore` by selecting the Angular as the UI framework and MongoDB as the database provider, create the database and run the application by following the [Getting Started document](../../Getting-Started-Angular-Template.md). |
|||
|
|||
### Solution Structure (Backend) |
|||
|
|||
This is how the layered solution structure looks after it's created: |
|||
|
|||
 |
|||
|
|||
> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial. |
|||
|
|||
### Create the Book Entity |
|||
|
|||
Domain layer in the startup template is separated into two projects: |
|||
|
|||
- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects. |
|||
- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients. |
|||
|
|||
Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: |
|||
|
|||
```C# |
|||
using System; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class Book : AuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices. |
|||
- `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. |
|||
- `Guid` is the **primary key type** of the `Book` entity. |
|||
|
|||
#### BookType Enum |
|||
|
|||
Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: |
|||
|
|||
```C# |
|||
namespace Acme.BookStore |
|||
{ |
|||
public enum BookType |
|||
{ |
|||
Undefined, |
|||
Adventure, |
|||
Biography, |
|||
Dystopia, |
|||
Fantastic, |
|||
Horror, |
|||
Science, |
|||
ScienceFiction, |
|||
Poetry |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### Add Book Entity to Your DbContext |
|||
|
|||
Add a `IMongoCollection` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: |
|||
|
|||
```csharp |
|||
public class BookStoreMongoDbContext : AbpMongoDbContext |
|||
{ |
|||
public IMongoCollection<Book> Books => Collection<Book>(); |
|||
... |
|||
} |
|||
``` |
|||
|
|||
#### Add Seed (Sample) Data |
|||
|
|||
This section is optional, but it would be good to have an initial data in the database in the first run. ABP provides a [data seed system](../../Data-Seeding.md). Create a class deriving from the `IDataSeedContributor` in the `.Domain` project: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreDataSeederContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
|
|||
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
if (await _bookRepository.GetCountAsync() > 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Name = "1984", |
|||
Type = BookType.Dystopia, |
|||
PublishDate = new DateTime(1949, 6, 8), |
|||
Price = 19.84f |
|||
} |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Name = "The Hitchhiker's Guide to the Galaxy", |
|||
Type = BookType.ScienceFiction, |
|||
PublishDate = new DateTime(1995, 9, 27), |
|||
Price = 42.0f |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
``` |
|||
|
|||
`BookStoreDataSeederContributor` simply inserts two books into database if there is no book added before. ABP automatically discovers and executes this class when you seed the database by running the `Acme.BookStore.DbMigrator` project. |
|||
|
|||
### Create the Application Service |
|||
|
|||
The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects: |
|||
|
|||
- `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces. |
|||
- `Acme.BookStore.Application` contains the implementations of your application services. |
|||
|
|||
#### BookDto |
|||
|
|||
Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: |
|||
|
|||
```C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookDto : AuditedEntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- **DTO** classes are used to **transfer data** between the _presentation layer_ and the _application layer_. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details. |
|||
- `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. |
|||
- `BookDto` is derived from the `AuditedEntityDto<Guid>` which has audit properties just like the `Book` class defined above. |
|||
|
|||
It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: |
|||
|
|||
```csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<Book, BookDto>(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### CreateUpdateBookDto |
|||
|
|||
Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: |
|||
|
|||
```c# |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class CreateUpdateBookDto |
|||
{ |
|||
[Required] |
|||
[StringLength(128)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public BookType Type { get; set; } = BookType.Undefined; |
|||
|
|||
[Required] |
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
[Required] |
|||
public float Price { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- This DTO class is used to get book information from the user interface while creating or updating a book. |
|||
- It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework. |
|||
|
|||
Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity: |
|||
|
|||
```csharp |
|||
CreateMap<CreateUpdateBookDto, Book>(); |
|||
``` |
|||
|
|||
#### IBookAppService |
|||
|
|||
Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: |
|||
|
|||
```C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public interface IBookAppService : |
|||
ICrudAppService< //Defines CRUD methods |
|||
BookDto, //Used to show books |
|||
Guid, //Primary key of the book entity |
|||
PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books |
|||
CreateUpdateBookDto, //Used to create a new book |
|||
CreateUpdateBookDto> //Used to update a book |
|||
{ |
|||
|
|||
} |
|||
} |
|||
``` |
|||
|
|||
- Defining interfaces for application services is <u>not required</u> by the framework. However, it's suggested as a best practice. |
|||
- `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. |
|||
- There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. |
|||
|
|||
#### BookAppService |
|||
|
|||
Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: |
|||
|
|||
```C# |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService : |
|||
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, |
|||
CreateUpdateBookDto, CreateUpdateBookDto>, |
|||
IBookAppService |
|||
{ |
|||
public BookAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above. |
|||
- `BookAppService` injects `IRepository<Book, Guid>` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md). |
|||
- `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected. |
|||
|
|||
### Auto API Controllers |
|||
|
|||
You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention. |
|||
|
|||
#### Swagger UI |
|||
|
|||
The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the `Acme.BookStore.HttpApi.Host` application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser. |
|||
|
|||
You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: |
|||
|
|||
 |
|||
|
|||
Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. |
|||
|
|||
### Create the Books Page |
|||
|
|||
In this tutorial; |
|||
|
|||
- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services |
|||
- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library |
|||
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. |
|||
- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). |
|||
|
|||
#### Install NPM Packages |
|||
|
|||
Open a terminal window and go to `angular` folder and then run `yarn` command for installing NPM packages: |
|||
|
|||
``` |
|||
yarn |
|||
``` |
|||
|
|||
#### BooksModule |
|||
|
|||
Run the following command line to create a new module, named `BooksModule`: |
|||
|
|||
```bash |
|||
yarn ng generate module books --route books --module app.module |
|||
``` |
|||
|
|||
 |
|||
|
|||
Run `yarn start`, wait Angular to run the application and open `http://localhost:4200/books` on a browser: |
|||
|
|||
 |
|||
|
|||
#### Routing |
|||
|
|||
Open the `app-routing.module.ts` and replace `books` as shown below: |
|||
|
|||
```typescript |
|||
import { LayoutApplicationComponent } from '@abp/ng.theme.basic';- |
|||
|
|||
//... |
|||
{ |
|||
path: 'books', |
|||
component: LayoutApplicationComponent, |
|||
loadChildren: () => import('./books/books.module').then(m => m.BooksModule), |
|||
data: { |
|||
routes: { |
|||
name: 'Books', |
|||
} as ABP.Route, |
|||
}, |
|||
}, |
|||
``` |
|||
|
|||
`LayoutApplicationComponent` configuration sets the application layout to the new page. If you would like to see your route on the navigation bar (main menu) you must also add the `data` object with `name` property in your route. |
|||
|
|||
 |
|||
|
|||
#### Book List Component |
|||
|
|||
First, replace the `books.component.html` to the following line to place the router-outlet: |
|||
|
|||
```html |
|||
<router-outlet></router-outlet> |
|||
``` |
|||
|
|||
Then run the command below on the terminal in the root folder to generate a new component, named book-list: |
|||
|
|||
```bash |
|||
yarn ng generate component books/book-list |
|||
``` |
|||
|
|||
 |
|||
|
|||
Import the `SharedModule` to the `BooksModule` to reuse some components and services defined in: |
|||
|
|||
```typescript |
|||
import { SharedModule } from '../shared/shared.module'; |
|||
|
|||
@NgModule({ |
|||
//... |
|||
imports: [ |
|||
//... |
|||
SharedModule, |
|||
], |
|||
}) |
|||
export class BooksModule {} |
|||
``` |
|||
|
|||
Then, update the `routes` in the `books-routing.module.ts` to add the new book-list component: |
|||
|
|||
```typescript |
|||
import { BookListComponent } from './book-list/book-list.component'; |
|||
|
|||
const routes: Routes = [ |
|||
{ |
|||
path: '', |
|||
component: BooksComponent, |
|||
children: [{ path: '', component: BookListComponent }], |
|||
}, |
|||
]; |
|||
|
|||
@NgModule({ |
|||
imports: [RouterModule.forChild(routes)], |
|||
exports: [RouterModule], |
|||
}) |
|||
export class BooksRoutingModule {} |
|||
``` |
|||
|
|||
 |
|||
|
|||
#### Create BooksState |
|||
|
|||
Run the following command in the terminal to create a new state, named `BooksState`: |
|||
|
|||
```shell |
|||
yarn ng generate ngxs-schematic:state books |
|||
``` |
|||
|
|||
This command creates several new files and edits `app.modules.ts` to import the `NgxsModule` with the new state: |
|||
|
|||
```typescript |
|||
// app.module.ts |
|||
|
|||
import { BooksState } from './store/states/books.state'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
//... |
|||
NgxsModule.forRoot([BooksState]), |
|||
], |
|||
//... |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
#### Get Books Data from Backend |
|||
|
|||
First, create data types to map data returning from the backend (you can check swagger UI or your backend API to know the data format). |
|||
|
|||
Modify the `books.ts` as shown below: |
|||
|
|||
```typescript |
|||
import { ABP } from '@abp/ng.core'; |
|||
|
|||
export namespace Books { |
|||
export interface State { |
|||
books: Response; |
|||
} |
|||
|
|||
export type Response = ABP.PagedResponse<Book>; |
|||
|
|||
export interface Book { |
|||
name: string; |
|||
type: Type; |
|||
publishDate: string; |
|||
price: number; |
|||
lastModificationTime: string; |
|||
lastModifierId: string; |
|||
creationTime: string; |
|||
creatorId: string; |
|||
id: string; |
|||
} |
|||
|
|||
export enum BookType { |
|||
Undefined, |
|||
Adventure, |
|||
Biography, |
|||
Dystopia, |
|||
Fantastic, |
|||
Horror, |
|||
Science, |
|||
ScienceFiction, |
|||
Poetry, |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Added `Book` interface that represents a book object and `BookType` enum represents a book category. |
|||
|
|||
#### BooksService |
|||
|
|||
Now, create a new service, named `BooksService` to perform HTTP calls to the server: |
|||
|
|||
```bash |
|||
yarn ng generate service books/shared/books |
|||
``` |
|||
|
|||
 |
|||
|
|||
Modify `book.service.ts` as shown below: |
|||
|
|||
```typescript |
|||
import { Injectable } from '@angular/core'; |
|||
import { RestService, Rest } from '@abp/ng.core'; |
|||
import { Books } from '../../store/models'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BooksService { |
|||
constructor(private rest: RestService) {} |
|||
|
|||
get(): Observable<Books.Response> { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/app/book', |
|||
}; |
|||
|
|||
return this.rest.request<null, Books.Response>(request); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Added the `get` method to get the list of books by performing an HTTP request to the related endpoint. |
|||
|
|||
Replace `books.actions.ts` content as shown below: |
|||
|
|||
```typescript |
|||
export class GetBooks { |
|||
static readonly type = '[Books] Get'; |
|||
} |
|||
``` |
|||
|
|||
#### Implement the BooksState |
|||
|
|||
Open the `books.state.ts` and change the file as shown below: |
|||
|
|||
```typescript |
|||
import { State, Action, StateContext, Selector } from '@ngxs/store'; |
|||
import { GetBooks } from '../actions/books.actions'; |
|||
import { Books } from '../models/books'; |
|||
import { BooksService } from '../../books/shared/books.service'; |
|||
import { tap } from 'rxjs/operators'; |
|||
|
|||
@State<Books.State>({ |
|||
name: 'BooksState', |
|||
defaults: { books: {} } as Books.State, |
|||
}) |
|||
export class BooksState { |
|||
@Selector() |
|||
static getBooks({ books }: Books.State) { |
|||
return books.items || []; |
|||
} |
|||
|
|||
constructor(private booksService: BooksService) {} |
|||
|
|||
@Action(GetBooks) |
|||
get({ patchState }: StateContext<Books.State>) { |
|||
return this.booksService.get().pipe( |
|||
tap(books => { |
|||
patchState({ |
|||
books, |
|||
}); |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Added the `GetBooks` action that uses the `BookService` defined above to get the books and patch the state. |
|||
|
|||
> NGXS requires to return the observable without subscribing it, as done in this sample (in the get function). |
|||
|
|||
#### BookListComponent |
|||
|
|||
Modify the `book-list.component.ts` as shown below: |
|||
|
|||
```typescript |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Store, Select } from '@ngxs/store'; |
|||
import { BooksState } from '../../store/states'; |
|||
import { Observable } from 'rxjs'; |
|||
import { Books } from '../../store/models'; |
|||
import { GetBooks } from '../../store/actions'; |
|||
|
|||
@Component({ |
|||
selector: 'app-book-list', |
|||
templateUrl: './book-list.component.html', |
|||
styleUrls: ['./book-list.component.scss'], |
|||
}) |
|||
export class BookListComponent implements OnInit { |
|||
@Select(BooksState.getBooks) |
|||
books$: Observable<Books.Book[]>; |
|||
|
|||
booksType = Books.BookType; |
|||
|
|||
loading = false; |
|||
|
|||
constructor(private store: Store) {} |
|||
|
|||
ngOnInit() { |
|||
this.loading = true; |
|||
this.store.dispatch(new GetBooks()) |
|||
.subscribe(() => { |
|||
this.loading = false |
|||
}; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> See the [Dispatching Actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the NGXS documentation for more information on these NGXS features. |
|||
|
|||
Replace `book-list.component.html` content as shown below: |
|||
|
|||
```html |
|||
<div id="wrapper" class="card"> |
|||
<div class="card-header"> |
|||
<div class="row"> |
|||
<div class="col col-md-6"> |
|||
<h5 class="card-title"> |
|||
Books |
|||
</h5> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
|||
<ng-template pTemplate="header"> |
|||
<tr> |
|||
<th>Book name</th> |
|||
<th>Book type</th> |
|||
<th>Publish date</th> |
|||
<th>Price</th> |
|||
</tr> |
|||
</ng-template> |
|||
<ng-template pTemplate="body" let-data> |
|||
<tr> |
|||
<td>{{ data.name }}</td> |
|||
<td>{{ booksType[data.type] }}</td> |
|||
<td>{{ data.publishDate | date }}</td> |
|||
<td>{{ data.price }}</td> |
|||
</tr> |
|||
</ng-template> |
|||
</p-table> |
|||
</div> |
|||
</div> |
|||
``` |
|||
|
|||
> We've used [PrimeNG table](https://www.primefaces.org/primeng/#/table) in this component. |
|||
|
|||
The resulting books page is shown below: |
|||
|
|||
 |
|||
|
|||
And this is the folder & file structure by the end of this tutorial: |
|||
|
|||
<img src="images/bookstore-angular-file-tree.png" height="75%"> |
|||
|
|||
> This tutorial follows the [Angular Style Guide](https://angular.io/guide/styleguide#file-tree). |
|||
|
|||
### Next Part |
|||
|
|||
See the [next part](Part-II.md) of this tutorial. |
|||
@ -0,0 +1,565 @@ |
|||
## Angular Tutorial - Part II |
|||
|
|||
### About this Tutorial |
|||
|
|||
This is the second part of the Angular tutorial series. See all parts: |
|||
|
|||
- [Part I: Create the project and a book list page](Part-I.md) |
|||
- **Part II: Create, Update and Delete books (this tutorial)** |
|||
|
|||
You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). |
|||
|
|||
### Creating a New Book |
|||
|
|||
In this section, you will learn how to create a new modal dialog form to create a new book. |
|||
|
|||
#### Type Definition |
|||
|
|||
Create an interface, named `CreateUpdateBookInput` in the `books.ts` as shown below: |
|||
|
|||
```typescript |
|||
export namespace Books { |
|||
//... |
|||
export interface CreateUpdateBookInput { |
|||
name: string; |
|||
type: BookType; |
|||
publishDate: string; |
|||
price: number; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`CreateUpdateBookInput` interface matches the `CreateUpdateBookDto` in the backend. |
|||
|
|||
#### Service Method |
|||
|
|||
Open the `books.service.ts` and add a new method, named `create` to perform an HTTP POST request to the server: |
|||
|
|||
```typescript |
|||
create(body: Books.CreateUpdateBookInput): Observable<Books.Book> { |
|||
const request: Rest.Request<Books.CreateUpdateBookInput> = { |
|||
method: 'POST', |
|||
url: '/api/app/book', |
|||
body, |
|||
}; |
|||
|
|||
return this.rest.request<Books.CreateUpdateBookInput, Books.Book>(request); |
|||
} |
|||
``` |
|||
|
|||
#### State Definitions |
|||
|
|||
Add the `CreateUpdateBook` action to `books.actions.ts` as shown below: |
|||
|
|||
```typescript |
|||
import { Books } from '../models'; |
|||
|
|||
export class CreateUpdateBook { |
|||
static readonly type = '[Books] Create Update Book'; |
|||
constructor(public payload: Books.CreateUpdateBookInput) {} |
|||
} |
|||
``` |
|||
|
|||
<!-- Added an `id` parameter to differentiate between create or update actions --> |
|||
|
|||
Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book: |
|||
|
|||
```typescript |
|||
@Action(CreateUpdateBook) |
|||
save({ dispatch }: StateContext<Books.State>, { payload }: CreateUpdateBook) { |
|||
return this.booksService |
|||
.create(payload) |
|||
.pipe(switchMap(() => dispatch(new GetBooks()))); |
|||
} |
|||
``` |
|||
|
|||
When the `SaveBook` action dispatched, the save method is executed. It call `create` method of the `BooksService` defined before. After the service call, `BooksState` dispatches the `GetBooks` action to get books again from the server to refresh the page. |
|||
|
|||
#### Add a Modal to BookListComponent |
|||
|
|||
Open the `book-list.component.html` and add the `abp-modal` to show/hide the book form. |
|||
|
|||
```html |
|||
<abp-modal [(visible)]="isModalOpen"> |
|||
<ng-template #abpHeader> |
|||
<h3>New Book</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> </ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
Cancel |
|||
</button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
`abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. |
|||
|
|||
Add a button, labeled `New book` to show the modal. |
|||
|
|||
```html |
|||
<div class="row"> |
|||
<div class="col col-md-6"> |
|||
<h5 class="card-title"> |
|||
Books |
|||
</h5> |
|||
</div> |
|||
<div class="text-right col col-md-6"> |
|||
<button id="create-role" class="btn btn-primary" type="button" (click)="onAdd()"> |
|||
<i class="fa fa-plus mr-1"></i> <span>New book</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
``` |
|||
|
|||
Open the `book-list.component.ts` and add `isModalOpen` variable and `onAdd` method to show/hide the modal. |
|||
|
|||
```typescript |
|||
isModalOpen = false; |
|||
|
|||
//... |
|||
|
|||
onAdd() { |
|||
this.isModalOpen = true; |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
#### Create a Reactive Form |
|||
|
|||
> [Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. |
|||
|
|||
Add a `form` variable and inject a `FormBuilder` service to the `book-list.component.ts` as shown below (remember add the import statement). |
|||
|
|||
```typescript |
|||
import { FormGroup, FormBuilder } from '@angular/forms'; |
|||
|
|||
form: FormGroup; |
|||
|
|||
constructor( |
|||
//... |
|||
private fb: FormBuilder |
|||
) {} |
|||
``` |
|||
|
|||
> The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. |
|||
|
|||
Add the `buildForm` method to create book form. |
|||
|
|||
```typescript |
|||
buildForm() { |
|||
this.form = this.fb.group({ |
|||
name: ['', Validators.required], |
|||
type: [null, Validators.required], |
|||
publishDate: [null, Validators.required], |
|||
price: [null, Validators.required], |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
- The `group` method of `FormBuilder` creates a `FormGroup`. |
|||
- Added `Validators.required` static method that validation of form element. |
|||
|
|||
Modify the `onAdd` method as shown below: |
|||
|
|||
```typescript |
|||
onAdd() { |
|||
this.buildForm(); |
|||
this.isModalOpen = true; |
|||
} |
|||
``` |
|||
|
|||
### Create the DOM Elements of the Form |
|||
|
|||
Open `book-list.component.html` and add the form in the body template of the modal. |
|||
|
|||
```html |
|||
<ng-template #abpBody> |
|||
<form [formGroup]="form"> |
|||
<div class="form-group"> |
|||
<label for="book-name">Name</label><span> * </span> |
|||
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus /> |
|||
</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]="booksType[type]" *ngFor="let type of bookTypeArr"> {{ type }}</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label>Publish date</label><span> * </span> |
|||
<input |
|||
#datepicker="ngbDatepicker" |
|||
class="form-control" |
|||
name="datepicker" |
|||
formControlName="publishDate" |
|||
ngbDatepicker |
|||
(click)="datepicker.toggle()" |
|||
/> |
|||
</div> |
|||
</form> |
|||
</ng-template> |
|||
``` |
|||
|
|||
TODO: Add a short description. |
|||
|
|||
> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. |
|||
|
|||
Open the `book-list.component.ts` and then add the `bookTypes`. |
|||
|
|||
```typescript |
|||
//... |
|||
form: FormGroup; |
|||
|
|||
bookTypeArr = Object.keys(Books.BookType).filter(bookType => typeof this.booksType[bookType] === 'number'); |
|||
``` |
|||
|
|||
The `bookTypes` variable added to generate array from `BookType` enum. The `bookTypes` equals like this: |
|||
|
|||
```js |
|||
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] |
|||
``` |
|||
|
|||
### Add the Datepicker Requirements |
|||
|
|||
Import `NgbDatepickerModule` to the `books.module.ts`. |
|||
|
|||
```typescript |
|||
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// ... |
|||
NgbDatepickerModule, |
|||
], |
|||
}) |
|||
export class BooksModule {} |
|||
``` |
|||
|
|||
Open the `book-list.component.html` and then add `providers` as shown below: |
|||
|
|||
```typescript |
|||
@Component({ |
|||
// ... |
|||
styleUrls: ['./book-list.component.scss'], |
|||
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], |
|||
}) |
|||
export class BookListComponent implements OnInit { |
|||
// ... |
|||
``` |
|||
|
|||
> The `NgbDateAdapter` convert Datepicker value type to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. |
|||
|
|||
 |
|||
|
|||
### Create a New Book |
|||
|
|||
Add the `save` method to `BookListComponent` |
|||
|
|||
```typescript |
|||
save() { |
|||
if (this.form.invalid) { |
|||
return; |
|||
} |
|||
|
|||
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { |
|||
this.isModalOpen = false; |
|||
this.form.reset(); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
TODO: description ?? |
|||
|
|||
Then, open the `book-list.component.html` and add the `abp-button` for the run `save` method. |
|||
|
|||
```html |
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
Cancel |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" (click)="save()">Save</abp-button> |
|||
</ng-template> |
|||
``` |
|||
|
|||
Now, You can add a new book. |
|||
|
|||
 |
|||
|
|||
### Add HTTP GET and PUT Methods |
|||
|
|||
TODO: Description |
|||
|
|||
Open the `book.service.ts` and then add the `getById` and `update` methods. |
|||
|
|||
```typescript |
|||
getById(id: string): Observable<Books.Book> { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: `/api/app/book/${id}`, |
|||
}; |
|||
|
|||
return this.rest.request<null, Books.Book>(request); |
|||
} |
|||
|
|||
update(body: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { |
|||
const request: Rest.Request<Books.CreateUpdateBookInput> = { |
|||
method: 'PUT', |
|||
url: `/api/app/book/${id}`, |
|||
body, |
|||
}; |
|||
|
|||
return this.rest.request<Books.CreateUpdateBookInput, Books.Book>(request); |
|||
} |
|||
``` |
|||
|
|||
- Added the `getById` method to get the editing book by performing an HTTP request to the related endpoint. |
|||
- Added the `update` method to update a book with the `id` by performing an HTTP request to the related endpoint. |
|||
|
|||
### Update a Book |
|||
|
|||
Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add variable named `selectedBook`. |
|||
|
|||
```typescript |
|||
selectedBook = {} as Books.Book; |
|||
|
|||
constructor( |
|||
//... |
|||
private booksService: BooksService |
|||
) |
|||
``` |
|||
|
|||
- Added `booksService` to get the detail of selected book by `id` before creating the form |
|||
- Added `selectedBook` variable to reuse detail of selected book. |
|||
|
|||
Modify the `buildForm` method to reuse the same form while editing a book. |
|||
|
|||
```typescript |
|||
buildForm() { |
|||
this.form = this.fb.group({ |
|||
name: [this.selectedBook.name || '', Validators.required], |
|||
type: this.selectedBook.type || null, |
|||
publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, |
|||
price: this.selectedBook.price || null, |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Add the `onEdit` method as shown below: |
|||
|
|||
```typescript |
|||
onEdit(id: string) { |
|||
this.booksService.getById(id).subscribe(book => { |
|||
this.selectedBook = book; |
|||
this.buildForm(); |
|||
this.isModalOpen = true; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
- Added `onEdit` method to get selected book detail, build form and then show the modal. |
|||
|
|||
Add the `selectedBook` definition to `onAdd` method for reuse same form while adding a new book. |
|||
|
|||
```typescript |
|||
onAdd() { |
|||
this.selectedBook = {} as Books.Item; |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
Add the `this.selectedBook.id` to `CreateUpdateBook` action in `save` method. |
|||
|
|||
```typescript |
|||
save() { |
|||
//... |
|||
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
Added the `this.selectedBook.id` property to reuse the `CreateUpdateBook` action. |
|||
|
|||
#### Add "Actions" Dropdown to the Table |
|||
|
|||
Open the `book-list.component.html` and add modify the `p-table` as shown below: |
|||
|
|||
```html |
|||
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
|||
<ng-template pTemplate="header"> |
|||
<tr> |
|||
<th>Actions</th> |
|||
<th>Book name</th> |
|||
<th>Book type</th> |
|||
<th>Publish date</th> |
|||
<th>Price</th> |
|||
</tr> |
|||
</ng-template> |
|||
<ng-template pTemplate="body" let-data> |
|||
<tr> |
|||
<td> |
|||
<div ngbDropdown class="d-inline-block"> |
|||
<button |
|||
class="btn btn-primary btn-sm dropdown-toggle" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
ngbDropdownToggle |
|||
> |
|||
<i class="fa fa-cog mr-1"></i>Actions |
|||
</button> |
|||
<div ngbDropdownMenu> |
|||
<button ngbDropdownItem (click)="onEdit(data.id)">Edit</button> |
|||
</div> |
|||
</div> |
|||
</td> |
|||
<td>{{ data.name }}</td> |
|||
<td>{{ booksType[data.type] }}</td> |
|||
<td>{{ data.publishDate | date }}</td> |
|||
<td>{{ data.price }}</td> |
|||
</tr> |
|||
</ng-template> |
|||
</p-table> |
|||
> |
|||
``` |
|||
|
|||
Actions button added to each row of the table. |
|||
|
|||
> We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. |
|||
|
|||
The Actions buttons looks like this: |
|||
|
|||
 |
|||
|
|||
Update the modal header for reuse the same modal. |
|||
|
|||
```html |
|||
<ng-template #abpHeader> |
|||
<h3>{{ (selectedBook.id ? 'Edit' : 'New Book') }}</h3> |
|||
</ng-template> |
|||
``` |
|||
|
|||
 |
|||
|
|||
TODO: header ?? |
|||
|
|||
Open the `books.actins.ts` and add `id` parameter. |
|||
|
|||
```typescript |
|||
export class CreateUpdateBook { |
|||
static readonly type = '[Books] Create Update Book'; |
|||
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} |
|||
} |
|||
``` |
|||
|
|||
Added `id` parameter to reuse the `BooksSave` while updating and creating a book. |
|||
|
|||
Open `books.state.ts` and then modify the `save` method as show below: |
|||
|
|||
```typescript |
|||
@Action(CreateUpdateBook) |
|||
save({ dispatch }: StateContext<Books.State>, { payload, id }: CreateUpdateBook) { |
|||
let request; |
|||
|
|||
if (id) { |
|||
request = this.booksService.update(payload, id); |
|||
} else { |
|||
request = this.booksService.create(payload); |
|||
} |
|||
|
|||
return request.pipe(switchMap(() => dispatch(new GetBooks()))); |
|||
} |
|||
``` |
|||
|
|||
TODO: description & screenshot ?? |
|||
|
|||
### Delete a Book |
|||
|
|||
Open `books.service.ts` and the the `delete` method for delete a book with the `id` by performing an HTTP request to the related endpoint. |
|||
|
|||
```typescript |
|||
delete(id: string): Observable<null> { |
|||
const request: Rest.Request<null> = { |
|||
method: 'DELETE', |
|||
url: `/api/app/book/${id}`, |
|||
}; |
|||
|
|||
return this.rest.request<null, null>(request); |
|||
} |
|||
``` |
|||
|
|||
### State Definitions |
|||
|
|||
Add an action named `BooksDelete` to `books.actions.ts` |
|||
|
|||
```typescript |
|||
export class DeleteBook { |
|||
static readonly type = '[Books] Delete'; |
|||
constructor(public id: string) {} |
|||
} |
|||
``` |
|||
|
|||
Then, open the `books.state.ts` and add the `delete` method that will listen to a `CreateUpdateBook` action to create a book |
|||
|
|||
```typescript |
|||
@Action(DeleteBook) |
|||
delete({ dispatch }: StateContext<Books.State>, { id }: DeleteBook) { |
|||
return this.booksService.delete(id).pipe(switchMap(() => dispatch(new GetBooks()))); |
|||
} |
|||
``` |
|||
|
|||
TODO: description?? |
|||
|
|||
### Add a Delete Button |
|||
|
|||
Open `book-list.component.html` and modify the `ngbDropdownMenu` for add the delete button as shown below: |
|||
|
|||
```html |
|||
<div ngbDropdownMenu> |
|||
... |
|||
<button ngbDropdownItem (click)="delete(data.id, data.name)"> |
|||
Delete |
|||
</button> |
|||
</div> |
|||
``` |
|||
|
|||
### Open Confirmation Popup |
|||
|
|||
Open `book-list.component.ts` and inject the `ConfirmationService` for show confirmation popup. |
|||
|
|||
```typescript |
|||
import { ConfirmationService } from '@abp/ng.theme.shared'; |
|||
//... |
|||
constructor( |
|||
//... |
|||
private confirmationService: ConfirmationService |
|||
) |
|||
``` |
|||
|
|||
Add the following method to `BookListComponent`. |
|||
|
|||
```typescript |
|||
import { ConfirmationService, Toaster } from '@abp/ng.theme.shared'; |
|||
//... |
|||
delete(id: string, name: string) { |
|||
this.confirmationService |
|||
.error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') |
|||
.subscribe(status => { |
|||
if (status === Toaster.Status.confirm) { |
|||
this.store.dispatch(new DeleteBook(id)); |
|||
} |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
The `delete` method shows confirmation popup and listens to them. When close the popup, the subscribe block runs. If confirmed this popup, it will dispatch the `DeleteBook` action. |
|||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 44 KiB |
@ -1,6 +0,0 @@ |
|||
(function ($) { |
|||
$('#GlobalRefreshButton').on('click', |
|||
function () { |
|||
$(document).trigger('RefreshWidgets', $('#DashboardGlobalFiltersForm').serializeFormToObject()); |
|||
}); |
|||
})(jQuery); |
|||
@ -1,27 +0,0 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:57333/", |
|||
"sslPort": 0 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"Volo.Abp.AspNetCore.Mvc.UI.Dashboards": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
}, |
|||
"applicationUrl": "http://localhost:57343/" |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<AssemblyName>Volo.Abp.AspNetCore.Mvc.UI.Dashboards</AssemblyName> |
|||
<PackageId>Volo.Abp.AspNetCore.Mvc.UI.Dashboards</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<IsPackable>true</IsPackable> |
|||
<OutputType>Library</OutputType> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Volo\Abp\AspNetCore\Mvc\UI\Dashboards\Components\**\*.cshtml" /> |
|||
<Content Remove="Volo\Abp\AspNetCore\Mvc\UI\Dashboards\Components\**\*.cshtml" /> |
|||
<EmbeddedResource Include="Pages\Components\Dashboard\*.js" /> |
|||
<Content Remove="Pages\Components\Dashboard\*.js" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Volo\Abp\AspNetCore\Mvc\UI\Dashboards\_ViewImports.cshtml" /> |
|||
<Content Remove="Volo\Abp\AspNetCore\Mvc\UI\Dashboards\_ViewImports.cshtml" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Content Include="Volo\Abp\AspNetCore\Mvc\UI\Dashboards\Components\Dashboard\Default.cshtml" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Widgets\Volo.Abp.AspNetCore.Mvc.UI.Widgets.csproj" /> |
|||
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Bundling\Volo.Abp.AspNetCore.Mvc.UI.Bundling.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -1,20 +0,0 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcUiWidgetsModule) |
|||
)] |
|||
public class AbpAspNetCoreMvcUiDashboardsModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<VirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAspNetCoreMvcUiDashboardsModule>(); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class AbpBasicDashboardScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.Add("/Pages/Components/Dashboard/Default.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class AbpBasicDashboardStyleContributor : BundleContributor |
|||
{ |
|||
} |
|||
} |
|||
@ -1,31 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard |
|||
{ |
|||
public class DashboardViewComponent : AbpViewComponent |
|||
{ |
|||
private readonly DashboardOptions _dashboardOptions; |
|||
private readonly WidgetOptions _widgetOptions; |
|||
private readonly GlobalFilterOptions _globalFilterOptions; |
|||
|
|||
public DashboardViewComponent(IOptions<DashboardOptions> dashboardOptions, IOptions<WidgetOptions> widgetOptions, IOptions<GlobalFilterOptions> globalFilterOptions) |
|||
{ |
|||
_dashboardOptions = dashboardOptions.Value; |
|||
_widgetOptions = widgetOptions.Value; |
|||
_globalFilterOptions = globalFilterOptions.Value; |
|||
} |
|||
|
|||
public IViewComponentResult Invoke(string dashboardName) |
|||
{ |
|||
var dashboard = _dashboardOptions.Dashboards.Single(d => d.Name.Equals(dashboardName)); |
|||
|
|||
var model = new DashboardViewModel(dashboard, _widgetOptions.Widgets.GetAll().ToList(), _globalFilterOptions.GlobalFilters); |
|||
|
|||
return View("~/Volo/Abp/AspNetCore/Mvc/UI/Dashboards/Components/Dashboard/Default.cshtml", model); |
|||
} |
|||
} |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard |
|||
{ |
|||
public class DashboardViewModel |
|||
{ |
|||
public DashboardDefinition Dashboard { get; set; } |
|||
|
|||
public List<WidgetDefinition> Widgets { get; set; } |
|||
|
|||
public List<GlobalFilterDefinition> GlobalFilters { get; set; } |
|||
|
|||
public DashboardViewModel(DashboardDefinition dashboard, List<WidgetDefinition> widgets, List<GlobalFilterDefinition> globalFilters) |
|||
{ |
|||
Dashboard = dashboard; |
|||
Widgets = widgets; |
|||
GlobalFilters = globalFilters; |
|||
} |
|||
|
|||
public WidgetDefinition GetWidget(string name) |
|||
{ |
|||
return Widgets.Single(d => d.Name.Equals(name)); |
|||
} |
|||
|
|||
public GlobalFilterDefinition GetGlobalFilter(string name) |
|||
{ |
|||
return GlobalFilters.Single(d => d.Name.Equals(name)); |
|||
} |
|||
|
|||
public async Task<bool> CheckPermissionsAsync(IAuthorizationService authorizationService, WidgetDefinition widget) |
|||
{ |
|||
foreach (var permission in widget.RequiredPolicies) |
|||
{ |
|||
if (!await authorizationService.IsGrantedAsync(permission)) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,53 +0,0 @@ |
|||
@using Localization.Resources.AbpUi |
|||
@using Microsoft.AspNetCore.Authorization |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Microsoft.Extensions.Localization |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets |
|||
@inject IAuthorizationService AuthorizationService |
|||
@inject IGlobalFilterRenderer GlobalFilteRenderer |
|||
@inject IHtmlLocalizer<AbpUiResource> L |
|||
@model Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard.DashboardViewModel |
|||
@{ |
|||
} |
|||
|
|||
<abp-row class="mb-4 align-items-center justify-content-between"> |
|||
@if (Model.Dashboard.AvailableGlobalFilters.Any()) |
|||
{ |
|||
<abp-column size="_10"> |
|||
<form id="DashboardGlobalFiltersForm"> |
|||
<abp-row> |
|||
@foreach (var globalFilterConfiguration in Model.Dashboard.AvailableGlobalFilters) |
|||
{ |
|||
var globalFilter = Model.GetGlobalFilter(globalFilterConfiguration.GlobalFilterName); |
|||
|
|||
@await GlobalFilteRenderer.RenderAsync(Component, globalFilter.Name) |
|||
|
|||
} |
|||
</abp-row> |
|||
</form> |
|||
</abp-column> |
|||
<abp-column size="_2"> |
|||
<div class="float-right"> |
|||
<abp-button button-type="Success" text="@L["Refresh"].Value" icon="refresh" id="GlobalRefreshButton" /> |
|||
</div> |
|||
</abp-column> |
|||
} |
|||
</abp-row> |
|||
|
|||
<abp-row> |
|||
@foreach (var widgetConfiguration in Model.Dashboard.AvailableWidgets) |
|||
{ |
|||
var widgetDefinition = Model.GetWidget(widgetConfiguration.WidgetName); |
|||
if (await Model.CheckPermissionsAsync(AuthorizationService, widgetDefinition)) |
|||
{ |
|||
widgetDefinition.DefaultDimensions = widgetConfiguration.Dimensions ?? widgetDefinition.DefaultDimensions ?? new WidgetDimensions(6, 4); |
|||
|
|||
<abp-column class="col-@(widgetDefinition.DefaultDimensions.Width) p-2" |
|||
style="height: @(widgetDefinition.DefaultDimensions.Height * 100)px"> |
|||
|
|||
@await Component.InvokeAsync(widgetDefinition.Name) |
|||
</abp-column> |
|||
} |
|||
} |
|||
</abp-row> |
|||
@ -1,62 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class DashboardDefinition |
|||
{ |
|||
/// <summary>
|
|||
/// Unique name of the dashboard.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
public string Name { get; } |
|||
|
|||
/// <summary>
|
|||
/// A list of Widgets available for this dashboard.
|
|||
/// </summary>
|
|||
public List<DashboardWidgetConfiguration> AvailableWidgets { get; } |
|||
|
|||
/// <summary>
|
|||
/// A list of Global Filters available for this dashboard.
|
|||
/// </summary>
|
|||
public List<DashboardGlobalFilterConfiguration> AvailableGlobalFilters { get; } |
|||
|
|||
/// <summary>
|
|||
/// Display name of the dashboard.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
public ILocalizableString DisplayName |
|||
{ |
|||
get => _displayName; |
|||
set => _displayName = Check.NotNull(value, nameof(value)); |
|||
} |
|||
private ILocalizableString _displayName; |
|||
|
|||
public DashboardDefinition( |
|||
[NotNull] string name, |
|||
[CanBeNull] ILocalizableString displayName) |
|||
{ |
|||
Name = Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
DisplayName = displayName ?? new FixedLocalizableString(name); |
|||
|
|||
AvailableWidgets = new List<DashboardWidgetConfiguration>(); |
|||
AvailableGlobalFilters = new List<DashboardGlobalFilterConfiguration>(); |
|||
} |
|||
|
|||
public DashboardDefinition WithWidget(string widgetName, WidgetLocation location = null, WidgetDimensions dimensions = null) |
|||
{ |
|||
AvailableWidgets.Add( new DashboardWidgetConfiguration(widgetName, dimensions, location)); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public DashboardDefinition WithGlobalFilter(string globalFilterName) |
|||
{ |
|||
AvailableGlobalFilters.Add( new DashboardGlobalFilterConfiguration(globalFilterName)); |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class DashboardGlobalFilterConfiguration |
|||
{ |
|||
[NotNull] |
|||
public string GlobalFilterName { get; } |
|||
|
|||
public DashboardGlobalFilterConfiguration([NotNull] string globalFilterName) |
|||
{ |
|||
GlobalFilterName = Check.NotNullOrWhiteSpace(globalFilterName, nameof(globalFilterName)); |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class DashboardOptions |
|||
{ |
|||
public List<DashboardDefinition> Dashboards { get; } |
|||
|
|||
public DashboardOptions() |
|||
{ |
|||
Dashboards = new List<DashboardDefinition>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Html; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Dashboards.Components.Dashboard; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class DashboardRenderer : IDashboardRenderer |
|||
{ |
|||
public async Task<IHtmlContent> RenderAsync(IViewComponentHelper componentHelper, object args = null) |
|||
{ |
|||
return await componentHelper.InvokeAsync(typeof(DashboardViewComponent), args ?? new object()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class DashboardWidgetConfiguration |
|||
{ |
|||
[NotNull] |
|||
public string WidgetName { get; } |
|||
|
|||
[CanBeNull] |
|||
public WidgetDimensions Dimensions { get; set; } |
|||
|
|||
[CanBeNull] |
|||
public WidgetLocation Location { get; set; } |
|||
|
|||
public DashboardWidgetConfiguration( |
|||
[NotNull] string widgetName, |
|||
[CanBeNull] WidgetDimensions dimensions = null, |
|||
[CanBeNull] WidgetLocation location = null |
|||
) |
|||
{ |
|||
WidgetName = Check.NotNullOrWhiteSpace(widgetName, nameof(widgetName)); |
|||
Dimensions = dimensions; |
|||
Location = location; |
|||
} |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class GlobalFilterDefinition |
|||
{ |
|||
/// <summary>
|
|||
/// Unique name of the Global Filter.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
public string Name { get; } |
|||
|
|||
/// <summary>
|
|||
/// Display name of the Global Filter.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
public ILocalizableString DisplayName |
|||
{ |
|||
get => _displayName; |
|||
set => _displayName = Check.NotNull(value, nameof(value)); |
|||
} |
|||
private ILocalizableString _displayName; |
|||
|
|||
[NotNull] |
|||
public Type ViewComponentType { get; } |
|||
|
|||
public GlobalFilterDefinition( |
|||
[NotNull] string name, |
|||
[CanBeNull] ILocalizableString displayName, |
|||
[NotNull] Type viewComponentType) |
|||
{ |
|||
Name = Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
DisplayName = displayName ?? new FixedLocalizableString(name); |
|||
ViewComponentType = Check.NotNull(viewComponentType, nameof(viewComponentType)); |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class GlobalFilterOptions |
|||
{ |
|||
public List<GlobalFilterDefinition> GlobalFilters { get; } |
|||
|
|||
public GlobalFilterOptions() |
|||
{ |
|||
GlobalFilters = new List<GlobalFilterDefinition>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,25 +0,0 @@ |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Html; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class GlobalFilterRenderer : IGlobalFilterRenderer |
|||
{ |
|||
private readonly GlobalFilterOptions _globalFilterOptions; |
|||
|
|||
public GlobalFilterRenderer(IOptions<GlobalFilterOptions> widgetOptions) |
|||
{ |
|||
_globalFilterOptions = widgetOptions.Value; |
|||
} |
|||
|
|||
public async Task<IHtmlContent> RenderAsync(IViewComponentHelper componentHelper, string globalFilterName, object args = null) |
|||
{ |
|||
var globalFilter = _globalFilterOptions.GlobalFilters.Single(w => w.Name.Equals(globalFilterName)); |
|||
|
|||
return await componentHelper.InvokeAsync(globalFilter.ViewComponentType, args ?? new object()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Html; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public interface IDashboardRenderer : ITransientDependency |
|||
{ |
|||
Task<IHtmlContent> RenderAsync(IViewComponentHelper componentHelper, object args = null); |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Html; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public interface IGlobalFilterRenderer : ITransientDependency |
|||
{ |
|||
Task<IHtmlContent> RenderAsync(IViewComponentHelper componentHelper, string globalFilterName, object args = null); |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Dashboards |
|||
{ |
|||
public class WidgetLocation |
|||
{ |
|||
public int X { get; set; } |
|||
|
|||
public int Y { get; set; } |
|||
|
|||
public WidgetLocation(int x, int y) |
|||
{ |
|||
X = x; |
|||
Y = y; |
|||
} |
|||
} |
|||
} |
|||
@ -1,3 +0,0 @@ |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@ -0,0 +1,37 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Pages.Abp.MultiTenancy |
|||
{ |
|||
[Route("api/abp/multi-tenancy")] |
|||
public class AbpTenantController : AbpController |
|||
{ |
|||
|
|||
protected ITenantStore TenantStore { get; } |
|||
|
|||
public AbpTenantController(ITenantStore tenantStore) |
|||
{ |
|||
TenantStore = tenantStore; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("find-tenant/{name}")] |
|||
public async Task<FindTenantResult> FindTenantAsync(string name) |
|||
{ |
|||
var tenant = await TenantStore.FindAsync(name); |
|||
|
|||
if (tenant == null) |
|||
{ |
|||
return new FindTenantResult{Success = false}; |
|||
} |
|||
|
|||
return new FindTenantResult |
|||
{ |
|||
Success = true, |
|||
TenantId = tenant.Id |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
|
|||
namespace Pages.Abp.MultiTenancy |
|||
{ |
|||
public class FindTenantResult |
|||
{ |
|||
public bool Success { get; set; } |
|||
|
|||
public Guid? TenantId { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.ChartJs |
|||
{ |
|||
public class ChartjsScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.Add("/libs/chart.js/Chart.js"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.ChartJs |
|||
{ |
|||
public class ChartjsStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.Add("/libs/chart.js/Chart.css"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
(function ($) { |
|||
abp.widgets = abp.widgets || {}; |
|||
|
|||
abp.WidgetManager = function (opts) { |
|||
if (!opts) { |
|||
opts = {}; |
|||
} else if (typeof opts === 'string') { |
|||
opts = { |
|||
wrapper: opts |
|||
}; |
|||
} |
|||
|
|||
if (!opts.wrapper) { |
|||
opts.wrapper = $('body'); |
|||
} else if (typeof opts.wrapper === 'string') { |
|||
opts.wrapper = $(opts.wrapper); |
|||
} |
|||
|
|||
if (!opts.filterForm) { |
|||
var widgetFilterAttr = opts.wrapper.attr('data-widget-filter'); |
|||
if (widgetFilterAttr) { |
|||
opts.filterForm = $(widgetFilterAttr); |
|||
} |
|||
} else if (typeof opts.filterForm === 'string') { |
|||
opts.filterForm = $(opts.filterForm); |
|||
} |
|||
|
|||
var getFilters = function ($widgetWrapperDiv) { |
|||
var filters = {}; |
|||
|
|||
if (opts.filterForm) { |
|||
opts.filterForm.each(function() { |
|||
filters = $.extend(filters, opts.filterForm.serializeFormToObject()); |
|||
}); |
|||
} |
|||
|
|||
if (opts.filterCallback) { |
|||
filters = $.extend(filters, opts.filterCallback()); |
|||
} |
|||
|
|||
var widgetApi = $widgetWrapperDiv.data('abp-widget-api'); |
|||
if (widgetApi && widgetApi.getFilters) { |
|||
filters = $.extend(filters, widgetApi.getFilters()); |
|||
} |
|||
|
|||
return filters; |
|||
}; |
|||
|
|||
var init = function () { |
|||
opts.wrapper.find('.abp-widget-wrapper').each(function () { |
|||
var $widgetWrapperDiv = $(this); |
|||
var widgetName = $widgetWrapperDiv.attr('data-widget-name'); |
|||
var widgetApiClass = abp.widgets[widgetName]; |
|||
if (widgetApiClass) { |
|||
var widgetApi = new widgetApiClass($widgetWrapperDiv); |
|||
$widgetWrapperDiv.data('abp-widget-api', widgetApi); |
|||
if (widgetApi.init) { |
|||
widgetApi.init(getFilters($widgetWrapperDiv)); |
|||
} |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
var refresh = function () { |
|||
opts.wrapper.find('.abp-widget-wrapper').each(function () { |
|||
var $widgetWrapperDiv = $(this); |
|||
|
|||
var refreshUrl = $widgetWrapperDiv.attr('data-refresh-url'); |
|||
if (refreshUrl) { |
|||
abp.ajax({ |
|||
url: refreshUrl, |
|||
type: 'GET', |
|||
dataType: 'html', |
|||
contentType: 'application/x-www-form-urlencoded; charset=UTF-8', |
|||
data: getFilters($widgetWrapperDiv) |
|||
}).then(function (result) { |
|||
$widgetWrapperDiv.replaceWith($(result)); |
|||
}); |
|||
} else { |
|||
var widgetApi = $widgetWrapperDiv.data('abp-widget-api'); |
|||
if (widgetApi && widgetApi.refresh) { |
|||
widgetApi.refresh(getFilters($widgetWrapperDiv)); |
|||
} |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
if (opts.filterForm) { |
|||
opts.filterForm.each(function() { |
|||
$(this).submit(function (e) { |
|||
e.preventDefault(); |
|||
refresh(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
var publicApi = { |
|||
init: init, |
|||
refresh: refresh |
|||
}; |
|||
|
|||
opts.wrapper.data('abp-widget-manager', publicApi); |
|||
|
|||
return publicApi; |
|||
}; |
|||
|
|||
})(jQuery); |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets |
|||
{ |
|||
public interface IWidgetManager : ITransientDependency |
|||
{ |
|||
Task<bool> IsGrantedAsync(Type widgetComponentType); |
|||
|
|||
Task<bool> IsGrantedAsync(string name); |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Widgets |
|||
{ |
|||
public class WidgetManager : IWidgetManager |
|||
{ |
|||
protected WidgetOptions Options { get; } |
|||
protected IAuthorizationService AuthorizationService { get; } |
|||
protected ICurrentUser CurrentUser { get; } |
|||
|
|||
public WidgetManager( |
|||
IOptions<WidgetOptions> widgetOptions, |
|||
IAuthorizationService authorizationService, |
|||
ICurrentUser currentUser) |
|||
{ |
|||
AuthorizationService = authorizationService; |
|||
CurrentUser = currentUser; |
|||
Options = widgetOptions.Value; |
|||
} |
|||
|
|||
public async Task<bool> IsGrantedAsync(Type widgetComponentType) |
|||
{ |
|||
var widget = Options.Widgets.Find(widgetComponentType); |
|||
|
|||
return await IsGrantedAsyncInternal(widget, widgetComponentType.FullName); |
|||
} |
|||
|
|||
public async Task<bool> IsGrantedAsync(string name) |
|||
{ |
|||
var widget = Options.Widgets.Find(name); |
|||
|
|||
return await IsGrantedAsyncInternal(widget, name); |
|||
} |
|||
|
|||
private async Task<bool> IsGrantedAsyncInternal(WidgetDefinition widget, string wantedWidgetName) |
|||
{ |
|||
if (widget == null) |
|||
{ |
|||
throw new ArgumentNullException(wantedWidgetName); |
|||
} |
|||
|
|||
if (widget.RequiredPolicies.Any()) |
|||
{ |
|||
foreach (var requiredPolicy in widget.RequiredPolicies) |
|||
{ |
|||
if (!(await AuthorizationService.AuthorizeAsync(requiredPolicy)).Succeeded) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
else if (widget.RequiresAuthentication && !CurrentUser.IsAuthenticated) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using Microsoft.AspNetCore.Cors.Infrastructure; |
|||
|
|||
namespace Microsoft.AspNetCore.Cors |
|||
{ |
|||
public static class AbpCorsPolicyBuilderExtensions |
|||
{ |
|||
public static CorsPolicyBuilder WithAbpExposedHeaders(this CorsPolicyBuilder corsPolicyBuilder) |
|||
{ |
|||
return corsPolicyBuilder.WithExposedHeaders("_AbpErrorFormat"); |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Volo.Abp.Cli.Commands |
|||
{ |
|||
public static class AddCommandHelper |
|||
{ |
|||
|
|||
} |
|||
} |
|||