@ -0,0 +1,13 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
"Account": "Račun", |
|||
"Welcome": "Dobrodošli", |
|||
"UseOneOfTheFollowingLinksToContinue": "Za nadaljevanje uporabite eno od naslednjih povezav", |
|||
"FrameworkHomePage": "Domača stran razvojnega okolja", |
|||
"FrameworkDocumentation": "Dokumentacija razvojnega okolja", |
|||
"OfficialBlog": "Uradni blog", |
|||
"CommercialHomePage": "Domača stran različice Commercial", |
|||
"CommercialSupportWebSite": "Spletna stran za komercialno podporo" |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
"Permission:Organizations": "Organizacije", |
|||
"Permission:Manage": "Upravljaj organizacije", |
|||
"Permission:NpmPackages": "NPM paketi", |
|||
"Permission:NugetPackages": "Nuget paketi", |
|||
"Permission:Maintenance": "Vzdrževanje", |
|||
"Permission:Maintain": "Vzdrževanje", |
|||
"Permission:ClearCaches": "Počisti predpomnilnike", |
|||
"Permission:Modules": "Moduli", |
|||
"Permission:Packages": "Paketi", |
|||
"Permission:Edit": "Urejanje", |
|||
"Permission:Delete": "Brisanje", |
|||
"Permission:Create": "Ustvarjanje", |
|||
"Menu:Organizations": "Organizacije", |
|||
"Menu:Packages": "Paketi", |
|||
"NpmPackageDeletionWarningMessage": "Ta paket NPM bo izbrisan. Ali to potrjujete?", |
|||
"NugetPackageDeletionWarningMessage": "Ta Nuget paket bo izbrisan. Ali to potrjujete?", |
|||
"ModuleDeletionWarningMessage": "Ta modul bo izbrisan. Ali to potrjujete?", |
|||
"Name": "Naziv", |
|||
"DisplayName": "Naziv za prikaz", |
|||
"ShortDescription": "Kratek opis", |
|||
"NameFilter": "Naziv", |
|||
"CreationTime": "Čas nastanka", |
|||
"IsPro": "Je pro", |
|||
"EfCoreConfigureMethodName": "Konfiguriraj ime metode", |
|||
"IsProFilter": "Je pro", |
|||
"ApplicationType": "Tip aplikacije", |
|||
"Target": "Cilj", |
|||
"TargetFilter": "Cilj", |
|||
"ModuleClass": "Razred modula", |
|||
"NugetPackageTarget.DomainShared": "Domain Shared", |
|||
"NugetPackageTarget.Domain": "Domain", |
|||
"NugetPackageTarget.Application": "Application", |
|||
"NugetPackageTarget.ApplicationContracts": "Application Contracts", |
|||
"NugetPackageTarget.HttpApi": "Http Api", |
|||
"NugetPackageTarget.HttpApiClient": "Http Api Client", |
|||
"NugetPackageTarget.Web": "Web", |
|||
"NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core", |
|||
"NugetPackageTarget.MongoDB": "MongoDB", |
|||
"Edit": "Uredi", |
|||
"Delete": "Izbriši", |
|||
"Refresh": "Osveži", |
|||
"NpmPackages": "NPM paketi", |
|||
"NugetPackages": "Nuget paketi", |
|||
"NpmPackageCount": "Število NPM paketov", |
|||
"NugetPackageCount": "Število Nuget paketov", |
|||
"Module": "Moduli", |
|||
"ModuleInfo": "Informacije o modulu", |
|||
"CreateANpmPackage": "Ustvari NPM paket", |
|||
"CreateAModule": "Ustvari modul", |
|||
"CreateANugetPackage": "Ustvari Nuget paket", |
|||
"AddNew": "Dodaj novega", |
|||
"PackageAlreadyExist{0}": "Paket \"{0}\" je že dodan.", |
|||
"ModuleAlreadyExist{0}": "Modul \"{0}\" je že dodan.", |
|||
"ClearCache": "Počisti predpomnilnik", |
|||
"SuccessfullyCleared": "Uspešno izbrisano", |
|||
"Menu:NpmPackages": "NPM paketi", |
|||
"Menu:Modules": "Moduli", |
|||
"Menu:Maintenance": "Vzdrževanje", |
|||
"Menu:NugetPackages": "Nuget paketi", |
|||
"CreateAnOrganization": "Ustvari organizacijo", |
|||
"Organizations": "Organizacije", |
|||
"LongName": "Dolg naziv", |
|||
"LicenseType": "Tip licence", |
|||
"LicenseStartTime": "Čas začetka licence", |
|||
"LicenseEndTime": "Čas konca licence", |
|||
"AllowedDeveloperCount": "Dovoljeno število razvijalcev", |
|||
"UserNameOrEmailAddress": "Uporabniško ime ali e-poštni naslov", |
|||
"AddOwner": "Dodaj lastnika", |
|||
"UserName": "Uporabniško ime", |
|||
"Email": "E-poštni naslov", |
|||
"Developers": "Razvijalci", |
|||
"AddDeveloper": "Dodaj razvijalca", |
|||
"Create": "Ustvari", |
|||
"UserNotFound": "Uporabnika ni mogoče najti", |
|||
"{0}WillBeRemovedFromMembers": "{0} bo odstranjen iz članov", |
|||
"Computers": "Računalniki", |
|||
"UniqueComputerId": "Unikatni id računalnika", |
|||
"LastSeenDate": "Zadnjič viden", |
|||
"{0}Computer{1}WillBeRemovedFromRecords": "Računalnik {0} ({1}) bo odstranjen iz zapisov", |
|||
"OrganizationDeletionWarningMessage": "Organizacija bo izbrisana", |
|||
"This{0}AlreadyExistInThisOrganization": "{0} že obstaja v tej organizaciji", |
|||
"AreYouSureYouWantToDeleteAllComputers": "Ali ste prepričani, da želite izbrisati vse računalnike?", |
|||
"DeleteAll": "Izbriši vse", |
|||
"DoYouWantToCreateNewUser": "Ali želite ustvariti novega uporabnika?", |
|||
"MasterModules": "Glavni moduli" |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
"Volo.AbpIo.Domain:010004": "Doseženo je največje število članov!", |
|||
"Volo.AbpIo.Domain:010005": "Doseženo je največje število lastnikov!", |
|||
"Volo.AbpIo.Domain:010006": "Ta uporabnik je že lastnik te organizacije!", |
|||
"Volo.AbpIo.Domain:010007": "Ta uporabnik je že razvijalec v tej organizaciji!", |
|||
"Volo.AbpIo.Domain:010008": "Dovoljeno število razvijalcev ne sme biti manjše od trenutnega števila razvijalcev!", |
|||
"Volo.AbpIo.Domain:010009": "Dovoljeno število razvijalcev ne sme biti manjše od 0!", |
|||
"Volo.AbpIo.Domain:010010": "Največje število mac naslovov je preseženo!", |
|||
"Volo.AbpIo.Domain:010011": "Osebne licence ne sme imeti več kot 1 razvijalec!", |
|||
"Volo.AbpIo.Domain:010012": "Licence ni mogoče podaljšati en mesec po poteku licence!", |
|||
"Volo.AbpIo.Domain:020001": "Tega paketa NPM ni mogoče izbrisati, ker so \"{NugetPackages}\" Nuget paketi odvisni od tega paketa.", |
|||
"Volo.AbpIo.Domain:020002": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", |
|||
"Volo.AbpIo.Domain:020003": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket in \"{NugetPackages}\" Nuget paketi so odvisni od tega paketa.", |
|||
"Volo.AbpIo.Domain:020004": "Nuget paketa ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", |
|||
"WantToLearn?": "Se želite naučiti?", |
|||
"ReadyToGetStarted?": "Pripravljeni, da bi začeli?", |
|||
"JoinOurCommunity": "Pridružite se naši skupnosti", |
|||
"GetStartedUpper": "ZAČNI", |
|||
"ForkMeOnGitHub": "Naredi vejitev na GitHub", |
|||
"Features": "Funkcionalnosti", |
|||
"GetStarted": "Začni", |
|||
"Documents": "Dokumenti", |
|||
"Community": "Skupnost", |
|||
"ContributionGuide": "Vodič za prispevke", |
|||
"Blog": "Blog", |
|||
"Commercial": "Commercial", |
|||
"SeeDocuments": "Poglej dokumente" |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
"OrganizationManagement": "Upravljanje organizacije", |
|||
"OrganizationList": "Seznam organizacij", |
|||
"Volo.AbpIo.Commercial:010003": "Niste lastnik te organizacije!", |
|||
"OrganizationNotFoundMessage": "Nobene organizacije ni bilo mogoče najti!", |
|||
"DeveloperCount": "Dodeljeni / skupno število razvijalcev", |
|||
"QuestionCount": "Preostala / skupno število vprašanj", |
|||
"Unlimited": "Neomejeno", |
|||
"Owners": "Lastniki", |
|||
"AddMember": "Dodaj člana", |
|||
"AddOwner": "Dodaj lastnika", |
|||
"AddDeveloper": "Dodaj razvijalce", |
|||
"UserName": "Uporabniško ime", |
|||
"Name": "Ime", |
|||
"EmailAddress": "E-poštni naslov", |
|||
"Developers": "Razvijalci", |
|||
"LicenseType": "Tip licence", |
|||
"Manage": "Upravljaj", |
|||
"StartDate": "Datum začetka", |
|||
"EndDate": "Datum konca", |
|||
"Modules": "Moduli", |
|||
"LicenseExtendMessage": "Končni datum veljavnosti licence je podaljšan na {0}", |
|||
"LicenseUpgradeMessage": "Vaša licenca je nadgrajena na {0}", |
|||
"LicenseAddDeveloperMessage": "{0} razvijalcev je bilo dodanih k vaši licenci", |
|||
"Volo.AbpIo.Commercial:010004": "Navedenega uporabnika ni mogoče najti! Uporabnik mora biti registriran.", |
|||
"MyOrganizations": "Moje organizacije", |
|||
"ApiKey": "Ključ API", |
|||
"UserNameNotFound": "Ni uporabnika z uporabniškim imenom {0}", |
|||
"SuccessfullyAddedToNewsletter": "Hvala, ker ste se naročili na naše novice!", |
|||
"ManageProfile": "Upravljaj svoj profil", |
|||
"EmailNotValid": "Vnesite veljaven e-poštni naslov." |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"culture": "sl", |
|||
"texts": { |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
# Auto API Controllers |
|||
|
|||
Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. |
|||
|
|||
ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. |
|||
|
|||
## Configuration |
|||
|
|||
Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: |
|||
|
|||
````csharp |
|||
[DependsOn(BookStoreApplicationModule)] |
|||
public class BookStoreWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options |
|||
.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). |
|||
|
|||
 |
|||
|
|||
### Examples |
|||
|
|||
Some example method names and the corresponding routes calculated by convention: |
|||
|
|||
| Service Method Name | HTTP Method | Route | |
|||
| ----------------------------------------------------- | ----------- | -------------------------- | |
|||
| GetAsync(Guid id) | GET | /api/app/book/{id} | |
|||
| GetListAsync() | GET | /api/app/book | |
|||
| CreateAsync(CreateBookDto input) | POST | /api/app/book | |
|||
| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | |
|||
| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | |
|||
| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | |
|||
| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | |
|||
|
|||
### HTTP Method |
|||
|
|||
ABP uses a naming convention while determining the HTTP method for a service method (action): |
|||
|
|||
- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. |
|||
- **Put**: Used if the method name starts with 'Put' or 'Update'. |
|||
- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. |
|||
- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. |
|||
- **Patch**: Used if the method name starts with 'Patch'. |
|||
- Otherwise, **Post** is used **by default**. |
|||
|
|||
If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. |
|||
|
|||
### Route |
|||
|
|||
Route is calculated based on some conventions: |
|||
|
|||
* It always starts with '**/api**'. |
|||
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: |
|||
|
|||
````csharp |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.RootPath = "volosoft/book-store"; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. |
|||
|
|||
* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. |
|||
* If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. |
|||
* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. |
|||
* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; |
|||
* Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. |
|||
* Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. |
|||
* Converting the result to **camelCase**. |
|||
* If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. |
|||
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. |
|||
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). |
|||
|
|||
## Service Selection |
|||
|
|||
Creating conventional HTTP API controllers are not unique to application services actually. |
|||
|
|||
### IRemoteService Interface |
|||
|
|||
If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. |
|||
|
|||
### RemoteService Attribute |
|||
|
|||
`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
### TypePredicate Option |
|||
|
|||
You can further filter classes to become an API controller by providing the `TypePredicate` option: |
|||
|
|||
````csharp |
|||
services.Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.TypePredicate = type => { return true; }; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. |
|||
|
|||
## API Explorer |
|||
|
|||
API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. |
|||
|
|||
API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsMetadataEnabled = false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. |
|||
@ -0,0 +1,165 @@ |
|||
# Dynamic C# API Clients |
|||
|
|||
ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. |
|||
|
|||
## Service Interface |
|||
|
|||
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: |
|||
|
|||
````csharp |
|||
public interface IBookAppService : IApplicationService |
|||
{ |
|||
Task<List<BookDto>> GetListAsync(); |
|||
} |
|||
```` |
|||
|
|||
Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. |
|||
|
|||
Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. |
|||
|
|||
## Client Proxy Generation |
|||
|
|||
First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Http.Client |
|||
```` |
|||
|
|||
Then add `AbpHttpClientModule` dependency to your module: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpHttpClientModule))] //add the dependency |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
Now, it's ready to create the client proxies. Example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule), //used to create client proxies |
|||
typeof(BookStoreApplicationModule) //contains the application service interfaces |
|||
)] |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//Create dynamic client proxies |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. |
|||
|
|||
### Endpoint Configuration |
|||
|
|||
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: |
|||
|
|||
```` |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See the "RemoteServiceOptions" section below for more detailed configuration. |
|||
|
|||
## Usage |
|||
|
|||
It's straightforward to use. Just inject the service interface in the client application code: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBookAppService _bookService; |
|||
|
|||
public MyService(IBookAppService bookService) |
|||
{ |
|||
_bookService = bookService; |
|||
} |
|||
|
|||
public async Task DoIt() |
|||
{ |
|||
var books = await _bookService.GetListAsync(); |
|||
foreach (var book in books) |
|||
{ |
|||
Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. |
|||
|
|||
### IHttpClientProxy Interface |
|||
|
|||
While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy<IBookAppService>` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy<T>` interface. |
|||
|
|||
## Configuration |
|||
|
|||
### RemoteServiceOptions |
|||
|
|||
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: |
|||
|
|||
````csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<AbpRemoteServiceOptions>(options => |
|||
{ |
|||
options.RemoteServices.Default = |
|||
new RemoteServiceConfiguration("http://localhost:53929/"); |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### Multiple Remote Service Endpoints |
|||
|
|||
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: |
|||
|
|||
````json |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
}, |
|||
"BookStore": { |
|||
"BaseUrl": "http://localhost:48392/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
remoteServiceName: "BookStore" |
|||
); |
|||
```` |
|||
|
|||
`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. |
|||
|
|||
### As Default Services |
|||
|
|||
When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
asDefaultServices: false |
|||
); |
|||
```` |
|||
|
|||
Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. |
|||
|
|||
> If you disable `asDefaultServices`, you can only use `IHttpClientProxy<T>` interface to use the client proxies (see the related section above). |
|||
@ -0,0 +1,3 @@ |
|||
# abp.auth JavaScript API |
|||
|
|||
TODO |
|||
@ -0,0 +1,24 @@ |
|||
# JavaScript API |
|||
|
|||
ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. |
|||
|
|||
## APIs |
|||
|
|||
* abp.ajax |
|||
* [abp.auth](Auth.md) |
|||
* abp.currentUser |
|||
* abp.dom |
|||
* abp.event |
|||
* abp.features |
|||
* abp.localization |
|||
* abp.log |
|||
* abp.ModalManager |
|||
* abp.notify |
|||
* abp.security |
|||
* abp.setting |
|||
* abp.ui |
|||
* abp.utils |
|||
* abp.ResourceLoader |
|||
* abp.WidgetManager |
|||
* Other APIs |
|||
|
|||
@ -1,140 +1,3 @@ |
|||
# Auto API Controllers |
|||
This document has moved. |
|||
|
|||
Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. |
|||
|
|||
ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. |
|||
|
|||
## Configuration |
|||
|
|||
Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: |
|||
|
|||
````csharp |
|||
[DependsOn(BookStoreApplicationModule)] |
|||
public class BookStoreWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options |
|||
.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). |
|||
|
|||
 |
|||
|
|||
### Examples |
|||
|
|||
Some example method names and the corresponding routes calculated by convention: |
|||
|
|||
| Service Method Name | HTTP Method | Route | |
|||
| ----------------------------------------------------- | ----------- | -------------------------- | |
|||
| GetAsync(Guid id) | GET | /api/app/book/{id} | |
|||
| GetListAsync() | GET | /api/app/book | |
|||
| CreateAsync(CreateBookDto input) | POST | /api/app/book | |
|||
| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | |
|||
| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | |
|||
| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | |
|||
| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | |
|||
|
|||
### HTTP Method |
|||
|
|||
ABP uses a naming convention while determining the HTTP method for a service method (action): |
|||
|
|||
- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. |
|||
- **Put**: Used if the method name starts with 'Put' or 'Update'. |
|||
- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. |
|||
- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. |
|||
- **Patch**: Used if the method name starts with 'Patch'. |
|||
- Otherwise, **Post** is used **by default**. |
|||
|
|||
If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. |
|||
|
|||
### Route |
|||
|
|||
Route is calculated based on some conventions: |
|||
|
|||
* It always starts with '**/api**'. |
|||
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: |
|||
|
|||
````csharp |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.RootPath = "volosoft/book-store"; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. |
|||
|
|||
* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. |
|||
* If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. |
|||
* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. |
|||
* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; |
|||
* Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. |
|||
* Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. |
|||
* Converting the result to **camelCase**. |
|||
* If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. |
|||
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. |
|||
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). |
|||
|
|||
## Service Selection |
|||
|
|||
Creating conventional HTTP API controllers are not unique to application services actually. |
|||
|
|||
### IRemoteService Interface |
|||
|
|||
If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. |
|||
|
|||
### RemoteService Attribute |
|||
|
|||
`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
### TypePredicate Option |
|||
|
|||
You can further filter classes to become an API controller by providing the `TypePredicate` option: |
|||
|
|||
````csharp |
|||
services.Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.TypePredicate = type => { return true; }; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. |
|||
|
|||
## API Explorer |
|||
|
|||
API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. |
|||
|
|||
API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsMetadataEnabled = false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. |
|||
[Click to navigate to Auto API Controllers document](../API/Auto-API-Controllers.md) |
|||
@ -1,352 +1,4 @@ |
|||
|
|||
# ASP.NET Core MVC Bundling & Minification |
|||
This document has moved. |
|||
|
|||
There are many ways of bundling & minification of client side resources (JavaScript and CSS files). Most common ways are: |
|||
|
|||
* Using the [Bundler & Minifier](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier) Visual Studio extension or the [NuGet package](https://www.nuget.org/packages/BuildBundlerMinifier/). |
|||
* Using [Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/) task managers and their plugins. |
|||
|
|||
ABP offers a simple, dynamic, powerful, modular and built-in way. |
|||
|
|||
## Volo.Abp.AspNetCore.Mvc.UI.Bundling Package |
|||
|
|||
> This package is already installed by default with the startup templates. So, most of the time, you don't need to install it manually. |
|||
|
|||
Install the `Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget package to your project: |
|||
|
|||
```` |
|||
install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
```` |
|||
|
|||
Then you can add the `AbpAspNetCoreMvcUiBundlingModule` dependency to your module: |
|||
|
|||
````C# |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Razor Bundling Tag Helpers |
|||
|
|||
The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-style-bundle` tag helpers. Example: |
|||
|
|||
````html |
|||
<abp-style-bundle name="MyGlobalBundle"> |
|||
<abp-style src="/libs/bootstrap/css/bootstrap.css" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
<abp-style src="/styles/my-global-style.css" /> |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
This bundle defines a style bundle with a **unique name**: `MyGlobalBundle`. It's very easy to understand how to use it. Let's see how it *works*: |
|||
|
|||
* ABP creates the bundle as **lazy** from the provided files when it's **first requested**. For the subsequent calls, it's returned from the **cache**. That means if you conditionally add the files to the bundle, it's executed only once and any changes of the condition will not effect the bundle for the next requests. |
|||
* ABP adds bundle files **individually** to the page for the `development` environment. It automatically bundles & minifies for other environments (`staging`, `production`...). |
|||
* The bundle files may be **physical** files or [**virtual/embedded** files](../Virtual-File-System.md). |
|||
* ABP automatically adds **version query string** to the bundle file URL to prevent browsers from caching when the bundle is being updated. (like ?_v=67872834243042 - generated from last change date of the related files). The versioning works even if the bundle files are individually added to the page (on the development environment). |
|||
|
|||
### Importing The Bundling Tag Helpers |
|||
|
|||
> This is already imported by default with the startup templates. So, most of the time, you don't need to add it manually. |
|||
|
|||
In order to use bundle tag helpers, you need to add it into your `_ViewImports.cshtml` file or into your page: |
|||
|
|||
```` |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
```` |
|||
|
|||
### Unnamed Bundles |
|||
|
|||
The `name` is **optional** for the razor bundle tag helpers. If you don't define a name, it's automatically **calculated** based on the used bundle file names (they are **concatenated** and **hashed**). Example: |
|||
|
|||
````html |
|||
<abp-style-bundle> |
|||
<abp-style src="/libs/bootstrap/css/bootstrap.css" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
@if (ViewBag.IncludeCustomStyles != false) |
|||
{ |
|||
<abp-style src="/styles/my-global-style.css" /> |
|||
} |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
This will potentially create **two different bundles** (one incudes the `my-global-style.css` and other does not). |
|||
|
|||
Advantages of **unnamed** bundles: |
|||
|
|||
* Can **conditionally add items** to the bundle. But this may lead to multiple variations of the bundle based on the conditions. |
|||
|
|||
Advantages of **named** bundles: |
|||
|
|||
* Other **modules can contribute** to the bundle by its name (see the sections below). |
|||
|
|||
### Single File |
|||
|
|||
If you need to just add a single file to the page, you can use the `abp-script` or `abp-style` tag without a wrapping in the `abp-script-bundle` or `abp-style-bundle` tag. Example: |
|||
|
|||
````xml |
|||
<abp-script src="/scripts/my-script.js" /> |
|||
```` |
|||
|
|||
The bundle name will be *scripts.my-scripts* for the example above ("/" is replaced by "."). All bundling features are work as expected for single file bundles too. |
|||
|
|||
## Bundling Options |
|||
|
|||
If you need to use same bundle in **multiple pages** or want to use some more **powerful features**, you can configure bundles **by code** in your [module](../Module-Development-Basics.md) class. |
|||
|
|||
### Creating A New Bundle |
|||
|
|||
Example usage: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Add("MyGlobalBundle", bundle => { |
|||
bundle.AddFiles( |
|||
"/libs/jquery/jquery.js", |
|||
"/libs/bootstrap/js/bootstrap.js", |
|||
"/libs/toastr/toastr.min.js", |
|||
"/scripts/my-global-scripts.js" |
|||
); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> You can use the same name (*MyGlobalBundle* here) for a script & style bundle since they are added to different collections (`ScriptBundles` and `StyleBundles`). |
|||
|
|||
After defining such a bundle, it can be included into a page using the same tag helpers defined above. Example: |
|||
|
|||
````html |
|||
<abp-script-bundle name="MyGlobalBundle" /> |
|||
```` |
|||
|
|||
This time, no file defined in the tag helper definition because the bundle files are defined by the code. |
|||
|
|||
### Configuring An Existing Bundle |
|||
|
|||
ABP supports [modularity](../Module-Development-Basics.md) for bundling as well. A module can modify an existing bundle that is created by a dependant module. Example: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(MyWebModule))] |
|||
public class MyWebExtensionModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Configure("MyGlobalBundle", bundle => { |
|||
bundle.AddFiles( |
|||
"/scripts/my-extension-script.js" |
|||
); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. |
|||
|
|||
## Bundle Contributors |
|||
|
|||
Adding files to an existing bundle seems useful. What if you need to **replace** a file in the bundle or you want to **conditionally** add files? Defining a bundle contributor provides extra power for such cases. |
|||
|
|||
An example bundle contributor that replaces bootstrap.css with a customized version: |
|||
|
|||
````C# |
|||
public class MyExtensionGlobalStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.ReplaceOne( |
|||
"/libs/bootstrap/css/bootstrap.css", |
|||
"/styles/extensions/bootstrap-customized.css" |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can use this contributor as like below: |
|||
|
|||
````C# |
|||
services.Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Configure("MyGlobalBundle", bundle => { |
|||
bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> You can also add contributors while creating a new bundle. |
|||
|
|||
Contributors can also be used in the bundle tag helpers. Example: |
|||
|
|||
````xml |
|||
<abp-style-bundle> |
|||
<abp-style type="@typeof(BootstrapStyleContributor)" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
`abp-style` and `abp-script` tags can get `type` attributes (instead of `src` attributes) as shown in this sample. When you add a bundle contributor, its dependencies are also automatically added to the bundle. |
|||
|
|||
### Contributor Dependencies |
|||
|
|||
A bundle contributor can have one or more dependencies to other contributors. |
|||
Example: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency |
|||
public class MyExtensionStyleBundleContributor : BundleContributor |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
When a bundle contributor is added, its dependencies are **automatically and recursively** added. Dependencies added by the **dependency order** by preventing **duplicates**. Duplicates are prevented even if they are in separated bundles. ABP organizes all bundles in a page and eliminates duplications. |
|||
|
|||
Creating contributors and defining dependencies is a way of organizing bundle creation across different modules. |
|||
|
|||
### Contributor Extensions |
|||
|
|||
In some advanced scenarios, you may want to do some additional configuration whenever a bundle contributor is used. Contributor extensions works seamlessly when the extended contributor is used. |
|||
|
|||
The example below adds some styles for prism.js library: |
|||
|
|||
````csharp |
|||
public class MyPrismjsStyleExtension : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. |
|||
|
|||
````csharp |
|||
Configure<BundleContributorOptions>(options => |
|||
{ |
|||
options |
|||
.Extensions<PrismjsStyleBundleContributor>() |
|||
.Add<MyPrismjsStyleExtension>(); |
|||
}); |
|||
```` |
|||
|
|||
Whenever `PrismjsStyleBundleContributor` is added into a bundle, `MyPrismjsStyleExtension` will also be automatically added. |
|||
|
|||
### Accessing to the IServiceProvider |
|||
|
|||
While it is rarely needed, `BundleConfigurationContext` has a `ServiceProvider` property that you can resolve service dependencies inside the `ConfigureBundle` method. |
|||
|
|||
### Standard Package Contributors |
|||
|
|||
Adding a specific NPM package resource (js, css files) into a bundle is pretty straight forward for that package. For example you always add the `bootstrap.css` file for the bootstrap NPM package. |
|||
|
|||
There are built-in contributors for all [standard NPM packages](Client-Side-Package-Management.md). For example, if your contributor depends on the bootstrap, you can just declare it, instead of adding the bootstrap.css yourself. |
|||
|
|||
````C# |
|||
[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency |
|||
public class MyExtensionStyleBundleContributor : BundleContributor |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
Using the built-in contributors for standard packages; |
|||
|
|||
* Prevents you typing **the invalid resource paths**. |
|||
* Prevents changing your contributor if the resource **path changes** (the dependant contributor will handle it). |
|||
* Prevents multiple modules adding the **duplicate files**. |
|||
* Manages **dependencies recursively** (adds dependencies of dependencies, if necessary). |
|||
|
|||
#### Volo.Abp.AspNetCore.Mvc.UI.Packages Package |
|||
|
|||
> This package is already installed by default in the startup templates. So, most of the time, you don't need to install it manually. |
|||
|
|||
Standard package contributors are defined in the `Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet package. |
|||
To install it to your project: |
|||
|
|||
```` |
|||
install-package Volo.Abp.AspNetCore.Mvc.UI.Packages |
|||
```` |
|||
|
|||
Then add the `AbpAspNetCoreMvcUiPackagesModule` module dependency to your own module; |
|||
|
|||
````C# |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Bundle Inheritance |
|||
|
|||
In some specific cases, it may be needed to create a **new** bundle **inherited** from other bundle(s). Inheriting from a bundle (recursively) inherits all files/contributors of that bundle. Then the derived bundle can add or modify files/contributors **without modifying** the original bundle. |
|||
Example: |
|||
|
|||
````c# |
|||
services.Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.StyleBundles |
|||
.Add("MyTheme.MyGlobalBundle", bundle => { |
|||
bundle |
|||
.AddBaseBundles("MyGlobalBundle") //Can add multiple |
|||
.AddFiles( |
|||
"/styles/mytheme-global-styles.css" |
|||
); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
## Themes |
|||
|
|||
Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. |
|||
|
|||
## Best Practices & Suggestions |
|||
|
|||
It's suggested to define multiple bundles for an application, each one is used for different purposes. |
|||
|
|||
* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. |
|||
* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. |
|||
* **Module bundles**: For shared resources among the pages of an individual module. |
|||
* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. |
|||
|
|||
Establish a balance between performance, network bandwidth usage and count of many bundles. |
|||
|
|||
## See Also |
|||
|
|||
* [Client Side Package Management](Client-Side-Package-Management.md) |
|||
* [Theming](Theming.md) |
|||
[Click to navigate to ASP.NET Core MVC Bundling & Minification document](../UI/AspNetCore/Bundling-Minification.md) |
|||
@ -1,116 +1,4 @@ |
|||
|
|||
## ASP.NET Core MVC Client Side Package Management |
|||
This document has moved. |
|||
|
|||
ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. |
|||
|
|||
However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. |
|||
|
|||
Finally, we suggest the [**Yarn**](https://yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. |
|||
|
|||
### @ABP NPM Packages |
|||
|
|||
ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. |
|||
|
|||
One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. |
|||
|
|||
To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [Github repository](https://github.com/volosoft/abp/tree/master/npm/packs). |
|||
|
|||
The benefit of a **standard package** is: |
|||
|
|||
* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. |
|||
* It contains the gulp task to copy library resources (js, css, img... files) from the **node_modules** folder to **wwwroot/libs** folder. See the *Mapping The Library Resources* section for more. |
|||
|
|||
Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: |
|||
|
|||
```` |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
"@abp/bootstrap": "^1.0.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
It's suggested to depend on a standard package instead of directly depending on a third-party package. |
|||
|
|||
#### Package Installation |
|||
|
|||
After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: |
|||
|
|||
```` |
|||
yarn |
|||
```` |
|||
|
|||
Alternatively, you can use `npm install` but [Yarn](https://yarnpkg.com/) is suggested as mentioned before. |
|||
|
|||
#### Package Contribution |
|||
|
|||
If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: |
|||
|
|||
* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). |
|||
* It should be the **latest stable** version of the package. |
|||
* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. |
|||
* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. |
|||
* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. |
|||
|
|||
See current standard packages for examples. |
|||
|
|||
### Mapping The Library Resources |
|||
|
|||
Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. |
|||
|
|||
Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. |
|||
|
|||
ABP defines a [Gulp](https://gulpjs.com/) based task to **copy resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. |
|||
|
|||
The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. |
|||
|
|||
#### Resource Mapping Definition File |
|||
|
|||
A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: |
|||
|
|||
````js |
|||
module.exports = { |
|||
aliases: { |
|||
"@node_modules": "./node_modules", |
|||
"@libs": "./wwwroot/libs" |
|||
}, |
|||
clean: [ |
|||
"@libs" |
|||
], |
|||
mappings: { |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. |
|||
* **clean** section is a list of folders to clean before copying the files. |
|||
* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. |
|||
|
|||
An example mapping configuration is shown below: |
|||
|
|||
````js |
|||
mappings: { |
|||
"@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", |
|||
"@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", |
|||
"@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" |
|||
} |
|||
```` |
|||
|
|||
#### Using The Gulp |
|||
|
|||
Once you properly configure the `abp.resourcemapping.js` file, you can run the gulp command from the command line: |
|||
|
|||
```` |
|||
gulp |
|||
```` |
|||
|
|||
When you run the `gulp`, all packages will copy their own resources into the **wwwroot/libs** folder. Running `yarn & gulp` is only necessary if you make a change in your dependencies in the **package.json** file. |
|||
|
|||
> When you run the Gulp command, dependencies of the application are resolved using the package.json file. The Gulp task automatically discovers and maps all resources from all dependencies (recursively). |
|||
|
|||
#### See Also |
|||
|
|||
* [Bundling & Minification](Bundling-Minification.md) |
|||
* [Theming](Theming.md) |
|||
[Click to navigate to ASP.NET Core MVC Client Side Package Management document](../UI/AspNetCore/Client-Side-Package-Management.md) |
|||
|
|||
@ -1,165 +1,3 @@ |
|||
# Dynamic C# API Clients |
|||
This document has moved. |
|||
|
|||
ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. |
|||
|
|||
## Service Interface |
|||
|
|||
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: |
|||
|
|||
````csharp |
|||
public interface IBookAppService : IApplicationService |
|||
{ |
|||
Task<List<BookDto>> GetListAsync(); |
|||
} |
|||
```` |
|||
|
|||
Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. |
|||
|
|||
Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. |
|||
|
|||
## Client Proxy Generation |
|||
|
|||
First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Http.Client |
|||
```` |
|||
|
|||
Then add `AbpHttpClientModule` dependency to your module: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpHttpClientModule))] //add the dependency |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
Now, it's ready to create the client proxies. Example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule), //used to create client proxies |
|||
typeof(BookStoreApplicationModule) //contains the application service interfaces |
|||
)] |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//Create dynamic client proxies |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. |
|||
|
|||
### Endpoint Configuration |
|||
|
|||
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: |
|||
|
|||
```` |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See the "RemoteServiceOptions" section below for more detailed configuration. |
|||
|
|||
## Usage |
|||
|
|||
It's straightforward to use. Just inject the service interface in the client application code: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBookAppService _bookService; |
|||
|
|||
public MyService(IBookAppService bookService) |
|||
{ |
|||
_bookService = bookService; |
|||
} |
|||
|
|||
public async Task DoIt() |
|||
{ |
|||
var books = await _bookService.GetListAsync(); |
|||
foreach (var book in books) |
|||
{ |
|||
Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. |
|||
|
|||
### IHttpClientProxy Interface |
|||
|
|||
While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy<IBookAppService>` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy<T>` interface. |
|||
|
|||
## Configuration |
|||
|
|||
### RemoteServiceOptions |
|||
|
|||
`RemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: |
|||
|
|||
````csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<RemoteServiceOptions>(options => |
|||
{ |
|||
options.RemoteServices.Default = |
|||
new RemoteServiceConfiguration("http://localhost:53929/"); |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### Multiple Remote Service Endpoints |
|||
|
|||
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: |
|||
|
|||
````json |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
}, |
|||
"BookStore": { |
|||
"BaseUrl": "http://localhost:48392/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
remoteServiceName: "BookStore" |
|||
); |
|||
```` |
|||
|
|||
`remoteServiceName` parameter matches the service endpoint configured via `RemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. |
|||
|
|||
### As Default Services |
|||
|
|||
When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
asDefaultServices: false |
|||
); |
|||
```` |
|||
|
|||
Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. |
|||
|
|||
> If you disable `asDefaultServices`, you can only use `IHttpClientProxy<T>` interface to use the client proxies (see the related section above). |
|||
[Click to navigate to Dynamic C# API Clients document](../API/Dynamic-CSharp-API-Clients.md) |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
# abp.auth JavaScript API |
|||
This document has moved. |
|||
|
|||
TODO |
|||
[Click to navigate to JavaScript Auth document](../../API/JavaScript-API/Auth.md) |
|||
@ -1,24 +1,3 @@ |
|||
# JavaScript API |
|||
|
|||
ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. |
|||
|
|||
## APIs |
|||
|
|||
* abp.ajax |
|||
* [abp.auth](Auth.md) |
|||
* abp.currentUser |
|||
* abp.dom |
|||
* abp.event |
|||
* abp.features |
|||
* abp.localization |
|||
* abp.log |
|||
* abp.ModalManager |
|||
* abp.notify |
|||
* abp.security |
|||
* abp.setting |
|||
* abp.ui |
|||
* abp.utils |
|||
* abp.ResourceLoader |
|||
* abp.WidgetManager |
|||
* Other APIs |
|||
This document has moved. |
|||
|
|||
[Click to navigate to JavaScript API document](../../API/JavaScript-API/Index.md) |
|||
@ -1,3 +1,3 @@ |
|||
## Dynamic Forms |
|||
This document has moved. |
|||
|
|||
This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. |
|||
[Click to navigate to Dynamic Forms document](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) |
|||
@ -1,3 +1,3 @@ |
|||
## ABP Tag Helpers |
|||
This document has moved. |
|||
|
|||
"ABP tag helpers" is not documented yet. You can see a [demo of components](http://bootstrap-taghelpers.abp.io/) for now. |
|||
[Click to navigate to ABP Tag Helpers document](../../UI/AspNetCore/Tag-Helpers/Index.md) |
|||
|
|||
@ -1,3 +1,4 @@ |
|||
# Theming |
|||
|
|||
TODO |
|||
This document has moved. |
|||
|
|||
[Click to navigate to Theming document](../UI/AspNetCore/Theming.md) |
|||
@ -1,505 +1,4 @@ |
|||
# Widgets |
|||
|
|||
ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; |
|||
This document has moved. |
|||
|
|||
* Have **scripts & styles** dependencies for your widget. |
|||
* Create **dashboards** with widgets used inside. |
|||
* Define widgets in reusable **[modules](../Module-Development-Basics.md)**. |
|||
* Co-operate widgets with **[authorization](../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. |
|||
|
|||
## Basic Widget Definition |
|||
|
|||
### Create a View Component |
|||
|
|||
As the first step, create a new regular ASP.NET Core View Component: |
|||
|
|||
 |
|||
|
|||
**MySimpleWidgetViewComponent.cs**: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. |
|||
|
|||
You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task<IViewComponentResult> InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. |
|||
|
|||
**Default.cshtml**: |
|||
|
|||
```xml |
|||
<div class="my-simple-widget"> |
|||
<h2>My Simple Widget</h2> |
|||
<p>This is a simple widget!</p> |
|||
</div> |
|||
``` |
|||
|
|||
### Define the Widget |
|||
|
|||
Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Rendering a Widget |
|||
|
|||
Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: |
|||
|
|||
````xml |
|||
@await Component.InvokeAsync("MySimpleWidget") |
|||
@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) |
|||
```` |
|||
|
|||
First approach uses the widget name while second approach uses the view component type. |
|||
|
|||
### Widgets with Arguments |
|||
|
|||
ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget |
|||
{ |
|||
[Widget] |
|||
public class CountersWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
private readonly IDashboardAppService _dashboardAppService; |
|||
|
|||
public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) |
|||
{ |
|||
_dashboardAppService = dashboardAppService; |
|||
} |
|||
|
|||
public async Task<IViewComponentResult> InvokeAsync( |
|||
DateTime startDate, DateTime endDate) |
|||
{ |
|||
var result = await _dashboardAppService.GetCountersWidgetAsync( |
|||
new CountersWidgetInputDto |
|||
{ |
|||
StartDate = startDate, |
|||
EndDate = endDate |
|||
} |
|||
); |
|||
|
|||
return View(result); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Now, you need to pass an anonymous object to pass arguments as shown below: |
|||
|
|||
````xml |
|||
@await Component.InvokeAsync("CountersWidget", new |
|||
{ |
|||
startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), |
|||
endDate = DateTime.Now |
|||
}) |
|||
```` |
|||
|
|||
## Widget Name |
|||
|
|||
Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. |
|||
|
|||
To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget] |
|||
[ViewComponent(Name = "MyCustomNamedWidget")] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
ABP will respect to the custom name by handling the widget. |
|||
|
|||
> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. |
|||
|
|||
### Display Name |
|||
|
|||
You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: |
|||
|
|||
````csharp |
|||
using DashboardDemo.Localization; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
DisplayName = "MySimpleWidgetDisplayName", //Localization key |
|||
DisplayNameResource = typeof(DashboardDemoResource) //localization resource |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See [the localization document](../Localization.md) to learn about localization resources and keys. |
|||
|
|||
## Style & Script Dependencies |
|||
|
|||
There are some challenges when your widget has script and style files; |
|||
|
|||
* Any page uses the widget should also include the **its script & styles** files into the page. |
|||
* The page should also care about **depended libraries/files** of the widget. |
|||
|
|||
ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. |
|||
|
|||
### Defining as Simple File Paths |
|||
|
|||
The example widget below adds a style and a script file: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, |
|||
ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../Virtual-File-System.md). |
|||
|
|||
### Defining Bundle Contributors |
|||
|
|||
All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. |
|||
|
|||
The sample code below does the same with the code above, but defines and uses bundle contributors: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, |
|||
ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
|
|||
public class MySimpleWidgetStyleBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files |
|||
.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); |
|||
} |
|||
} |
|||
|
|||
public class MySimpleWidgetScriptBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files |
|||
.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. |
|||
|
|||
See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. |
|||
|
|||
## RefreshUrl |
|||
|
|||
A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). |
|||
|
|||
````csharp |
|||
[Widget(RefreshUrl = "Widgets/Counters")] |
|||
public class CountersWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: |
|||
|
|||
````csharp |
|||
[Route("Widgets")] |
|||
public class CountersWidgetController : AbpController |
|||
{ |
|||
[HttpGet] |
|||
[Route("Counters")] |
|||
public IActionResult Counters(DateTime startDate, DateTime endDate) |
|||
{ |
|||
return ViewComponent("CountersWidget", new {startDate, endDate}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`Widgets/Counters` route matches to the `RefreshUrl` declared before. |
|||
|
|||
> A widget supposed to be refreshed in two ways: In the first way, when you use a `RefreshUrl`, it re-rendered on the server and replaced by the HTML returned from server. In the second way the widget gets data (generally a JSON object) from server and refreshes itself in the client side (see the refresh method in the Widget JavaScript API section). |
|||
|
|||
## JavaScript API |
|||
|
|||
A widget may need to be rendered and refreshed in the client side. In such cases, you can use ABP's `WidgetManager` and define APIs for your widgets. |
|||
|
|||
### WidgetManager |
|||
|
|||
`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: |
|||
|
|||
````js |
|||
$(function() { |
|||
var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); |
|||
}) |
|||
```` |
|||
|
|||
`MyDashboardWidgetsArea` may contain one or more widgets inside. |
|||
|
|||
> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. |
|||
|
|||
#### WidgetManager.init() |
|||
|
|||
`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) |
|||
|
|||
```js |
|||
myWidgetManager.init(); |
|||
``` |
|||
|
|||
#### WidgetManager.refresh() |
|||
|
|||
`refresh` method refreshes all widgets related to this `WidgetManager`: |
|||
|
|||
```` |
|||
myWidgetManager.refresh(); |
|||
```` |
|||
|
|||
#### WidgetManager Options |
|||
|
|||
WidgetManager has some additional options. |
|||
|
|||
##### Filter Form |
|||
|
|||
If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: |
|||
|
|||
````xml |
|||
<form method="get" id="MyDashboardFilterForm"> |
|||
...form elements |
|||
</form> |
|||
|
|||
<div id="MyDashboardWidgetsArea" data-widget-filter="#MyDashboardFilterForm"> |
|||
...widgets |
|||
</div> |
|||
```` |
|||
|
|||
`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. |
|||
|
|||
Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: |
|||
|
|||
````js |
|||
var myWidgetManager = new abp.WidgetManager({ |
|||
wrapper: '#MyDashboardWidgetsArea', |
|||
filterForm: '#MyDashboardFilterForm' |
|||
}); |
|||
```` |
|||
|
|||
##### Filter Callback |
|||
|
|||
You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: |
|||
|
|||
````js |
|||
var myWidgetManager = new abp.WidgetManager({ |
|||
wrapper: '#MyDashboardWidgetsArea', |
|||
filterCallback: function() { |
|||
return $('#MyDashboardFilterForm').serializeFormToObject(); |
|||
} |
|||
}); |
|||
```` |
|||
|
|||
This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: |
|||
|
|||
````js |
|||
filterCallback: function() { |
|||
return { |
|||
'startDate': $('#StartDateInput').val(), |
|||
'endDate': $('#EndDateInput').val() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
The returning filters are passed to all widgets on `init` and `refresh`. |
|||
|
|||
### Widget JavaScript API |
|||
|
|||
A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. |
|||
|
|||
````js |
|||
(function () { |
|||
abp.widgets.NewUserStatisticWidget = function ($wrapper) { |
|||
|
|||
var getFilters = function () { |
|||
return { |
|||
... |
|||
}; |
|||
} |
|||
|
|||
var refresh = function (filters) { |
|||
... |
|||
}; |
|||
|
|||
var init = function (filters) { |
|||
... |
|||
}; |
|||
|
|||
return { |
|||
getFilters: getFilters, |
|||
init: init, |
|||
refresh: refresh |
|||
}; |
|||
}; |
|||
})(); |
|||
```` |
|||
|
|||
`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. |
|||
|
|||
#### getFilters |
|||
|
|||
If the widget has internal custom filters, this function should return the filter object. Example: |
|||
|
|||
````js |
|||
var getFilters = function() { |
|||
return { |
|||
frequency: $wrapper.find('.frequency-filter option:selected').val() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
This method is used by the `WidgetManager` while building filters. |
|||
|
|||
#### init |
|||
|
|||
Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. |
|||
|
|||
#### refresh |
|||
|
|||
Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. |
|||
|
|||
## Authorization |
|||
|
|||
Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: |
|||
|
|||
* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). |
|||
* `RequiredPolicies` (`List<string>`): A list of policy names to authorize the user. See [the authorization document](../Authorization.md) for more info about policies. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget(RequiredPolicies = new[] { "MyPolicyName" })] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## WidgetOptions |
|||
|
|||
As alternative to the `Widget` attribute, you can use the `WidgetOptions` to configure widgets: |
|||
|
|||
```csharp |
|||
Configure<WidgetOptions>(options => |
|||
{ |
|||
options.Widgets.Add<MySimpleWidgetViewComponent>(); |
|||
}); |
|||
``` |
|||
|
|||
Write this into the `ConfigureServices` method of your [module](../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `WidgetOptions`. Example configuration that adds a style for the widget: |
|||
|
|||
````csharp |
|||
Configure<WidgetOptions>(options => |
|||
{ |
|||
options.Widgets |
|||
.Add<MySimpleWidgetViewComponent>() |
|||
.WithStyles("/Pages/Components/MySimpleWidget/Default.css"); |
|||
}); |
|||
```` |
|||
|
|||
> Tip: `WidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. |
|||
|
|||
## See Also |
|||
|
|||
* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). |
|||
[Click to navigate to Widgets document](../UI/AspNetCore/Widgets.md) |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
# Quartz Background Job Manager |
|||
|
|||
[Quartz](https://www.quartz-scheduler.net/) is an advanced background job manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background job manager](Background-Jobs.md). In this way, you can use the same background job API for Quartz and your code will be independent of Quartz. If you like, you can directly use Quartz's API, too. |
|||
|
|||
> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the Quartz integration. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.BackgroundJobs.Quartz |
|||
```` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet package to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.BackgroundJobs.Quartz |
|||
```` |
|||
|
|||
2. Add the `AbpBackgroundJobsQuartzModule` to the dependency list of your module: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
## Configuration |
|||
|
|||
Quartz is a very configurable library,and the ABP framework provides `AbpQuartzPreOptions` for this. You can use the `PreConfigure` method in your module class to pre-configure this option. ABP will use it when initializing the Quartz module. For example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
PreConfigure<AbpQuartzPreOptions>(options => |
|||
{ |
|||
options.Properties = new NameValueCollection |
|||
{ |
|||
["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp", |
|||
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", |
|||
["quartz.jobStore.tablePrefix"] = "QRTZ_", |
|||
["quartz.serializer.type"] = "json", |
|||
["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"), |
|||
["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer", |
|||
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Quartz stores job and scheduling information **in memory by default**. In the example, we use the pre-configuration of [options pattern](Options.md) to change it to the database. For more configuration of Quartz, please refer to the Quartz's [documentation](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html). |
|||
@ -0,0 +1,68 @@ |
|||
# Quartz Background Worker Manager |
|||
|
|||
[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Worker.md). ABP simply integrates quartz. |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. |
|||
|
|||
### Using the ABP CLI |
|||
|
|||
Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.BackgroundWorkers.Quartz |
|||
```` |
|||
|
|||
### Manual Installation |
|||
|
|||
If you want to manually install; |
|||
|
|||
1. Add the [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet package to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.BackgroundWorkers.Quartz |
|||
```` |
|||
|
|||
2. Add the `AbpBackgroundWorkersQuartzModule` to the dependency list of your module: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
//...other dependencies |
|||
typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency |
|||
)] |
|||
public class YourModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
### Configuration |
|||
|
|||
See [Configuration](Background-Jobs-Quartz#Configuration). |
|||
|
|||
### Create a Background Worker |
|||
|
|||
A background work is a class that derives from the `QuartzBackgroundWorkerBase` base class. for example. A simple worker class is shown below: |
|||
|
|||
```` csharp |
|||
public class MyLogWorker : QuartzBackgroundWorkerBase |
|||
{ |
|||
public MyLogWorker() |
|||
{ |
|||
JobDetail = JobBuilder.Create<MyLogWorker>().Build(); |
|||
Trigger = TriggerBuilder.Create().StartNow().Build(); |
|||
} |
|||
|
|||
public override Task Execute(IJobExecutionContext context) |
|||
{ |
|||
Logger.LogInformation("Executed MyLogWorker..!"); |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
We simply implemented the Execute method to write a log. The background worker is a **singleton by default**. If you want, you can also implement a [dependency interface](Dependency-Injection#DependencyInterfaces) to register it as another life cycle. |
|||
|
|||
### More |
|||
|
|||
Please see Quartz's [documentation](https://www.quartz-scheduler.net/documentation/index.html) for more information. |
|||
@ -0,0 +1,3 @@ |
|||
# Clock |
|||
|
|||
TODO |
|||
@ -0,0 +1,149 @@ |
|||
# Customizing the Application Modules: Extending Entities |
|||
|
|||
In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. |
|||
|
|||
## Extra Properties |
|||
|
|||
[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
//SET AN EXTRA PROPERTY |
|||
var user = await _identityUserRepository.GetAsync(userId); |
|||
user.SetProperty("Title", "My custom title value!"); |
|||
await _identityUserRepository.UpdateAsync(user); |
|||
|
|||
//GET AN EXTRA PROPERTY |
|||
var user = await _identityUserRepository.GetAsync(userId); |
|||
return user.GetProperty<string>("Title"); |
|||
```` |
|||
|
|||
This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). |
|||
|
|||
Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. |
|||
|
|||
See the [entities document](Entities.md) for more about the extra properties system. |
|||
|
|||
> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below. |
|||
|
|||
## Creating a New Entity Maps to the Same Database Table/Collection |
|||
|
|||
While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). |
|||
|
|||
Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). |
|||
|
|||
`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. |
|||
|
|||
## Creating a New Entity with Its Own Database Table/Collection |
|||
|
|||
Mapping your entity to an **existing table** of a depended module has a few disadvantages; |
|||
|
|||
* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. |
|||
* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. |
|||
|
|||
If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. |
|||
|
|||
In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; |
|||
|
|||
* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. |
|||
* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. |
|||
|
|||
Once you handle the event, you can update your own entity in your own database. |
|||
|
|||
### Subscribing to Local Events |
|||
|
|||
[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. |
|||
|
|||
Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler<EntityChangedEventData<IdentityUser>>` interface. |
|||
|
|||
````csharp |
|||
public class MyLocalIdentityUserChangeEventHandler : |
|||
ILocalEventHandler<EntityChangedEventData<IdentityUser>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityChangedEventData<IdentityUser> eventData) |
|||
{ |
|||
var userId = eventData.Entity.Id; |
|||
var userName = eventData.Entity.UserName; |
|||
|
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `EntityChangedEventData<T>` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). |
|||
* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData<T>` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. |
|||
|
|||
> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). |
|||
|
|||
### Subscribing to Distributed Events |
|||
|
|||
[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. |
|||
|
|||
Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below: |
|||
|
|||
````csharp |
|||
public class MyDistributedIdentityUserChangeEventHandler : |
|||
IDistributedEventHandler<EntityCreatedEto<EntityEto>>, |
|||
IDistributedEventHandler<EntityUpdatedEto<EntityEto>>, |
|||
IDistributedEventHandler<EntityDeletedEto<EntityEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityCreatedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "created" event |
|||
} |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityUpdatedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "updated" event |
|||
} |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityDeletedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "deleted" event |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus. |
|||
* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity). |
|||
|
|||
Pre-built application modules do not define specialized event types yet (like `IdentityUserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example: |
|||
|
|||
````csharp |
|||
public class MyDistributedIdentityUserCreatedEventHandler : |
|||
IDistributedEventHandler<EntityCreatedEto<IdentityUserEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityCreatedEto<IdentityUserEto> eventData) |
|||
{ |
|||
var userId = eventData.Entity.Id; |
|||
var userName = eventData.Entity.UserName; |
|||
//...handle the "created" event |
|||
} |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
* This handler is executed only when a new user has been created. |
|||
|
|||
> The only pre-defined specialized event class is the `UserEto`. For example, you can subscribe to the `EntityCreatedEto<UserEto>` to get notified when a user has created. This event also works for the Identity module. |
|||
|
|||
## See Also |
|||
|
|||
* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) |
|||
@ -0,0 +1,62 @@ |
|||
# Customizing the Existing Modules |
|||
|
|||
ABP Framework provides was designed to support to build fully [modular applications](Module-Development-Basics.md) and systems. It also provides some [pre-built application modules](Modules/Index.md) those are **ready to use** in any kind of application. |
|||
|
|||
For example, you can **re-use** the [Identity Management Module](Modules/Identity.md) to add user, role and permission management to your application. The [application startup template](Startup-Templates/Application.md) already comes with Identity and some other modules **pre-installed**. |
|||
|
|||
## Re-Using an Application Module |
|||
|
|||
You have two options to re-use an application module. |
|||
|
|||
### As Package References |
|||
|
|||
You can add **NuGet** & **NPM** package references of the related module to your application and configure the module (based on its documentation) to integrate to your application. |
|||
|
|||
As mentioned before, the [application startup template](Startup-Templates/Application.md) already comes with some **fundamental modules pre-installed**. It uses the modules as NuGet & NPM package references. |
|||
|
|||
This approach has the following benefits: |
|||
|
|||
* Your solution will be **clean** and only contains your **own application code**. |
|||
* You can **easily upgrade** a module when a new version is available. `abp update` [CLI](CLI.md) command makes it even easier. In this way, you can continue to get **new features and bug fixes**. |
|||
|
|||
However, there is a drawback: |
|||
|
|||
* You may not able to **customize** the module source code as it is in your own solution. |
|||
|
|||
This document explains **how to customize or extend** a depended module without need to change its source code. While it is limited compared to a full source code change opportunity, there are still some good ways to make some customizations. |
|||
|
|||
If you don't think to make huge changes on the pre-built modules, re-using them as package reference is the recommended way. |
|||
|
|||
### Including the Source Code |
|||
|
|||
If you want to make **huge changes** or add **major features** on a pre-built module, but the available extension points are not enough, you can consider to directly work the source code of the depended module. |
|||
|
|||
In this case, you typically **add the source code** of the module to your solution and **replace package references** by local project references. **[ABP CLI](CLI.md)** automates this process for you. |
|||
|
|||
#### Separating the Module Solution |
|||
|
|||
You may prefer to not include the module source code **directly into your solution**. Every module consists of 10+ project files and adding **multiple modules** may impact on the **size** of your solution **load & development time.** Also, you may have different development teams working on different modules, so you don't want to make the module code available to the application development team. |
|||
|
|||
In any case, you can create a **separate solution** for the desired module and depend on the module as project references out of the solution. We do it like that for the [abp repository](https://github.com/abpframework/abp/). |
|||
|
|||
> One problem we see is Visual Studio doesn't play nice with this kind of approach (it doesn't support well to have references to local projects out of the solution directory). If you get error while building the application (depends on an external module), run `dotnet restore` in the command line after opening the application's solution in the Visual Studio. |
|||
|
|||
#### Publishing the Customized Module as Packages |
|||
|
|||
One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company. |
|||
|
|||
## Module Customization / Extending Approaches |
|||
|
|||
This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways: |
|||
|
|||
* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) |
|||
* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) |
|||
* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) |
|||
|
|||
### See Also |
|||
|
|||
Also, see the following documents: |
|||
|
|||
* See [the localization document](Localization.md) to learn how to extend existing localization resources. |
|||
* See [the settings document](Settings.md) to learn how to change setting definitions of a depended module. |
|||
* See [the authorization document](Authorization.md) to learn how to change permission definitions of a depended module. |
|||
@ -0,0 +1,166 @@ |
|||
# Customizing the Application Modules: Overriding Services |
|||
|
|||
You may need to **change behavior (business logic)** of a depended module for your application. In this case, you can use the power of the [dependency injection system](Dependency-Injection.md) to replace a service, controller or even a page model of the depended module by your own implementation. |
|||
|
|||
**Replacing a service** is possible for any type of class registered to the dependency injection, including services of the ABP Framework. |
|||
|
|||
You have different options can be used based on your requirement those will be explained in the next sections. |
|||
|
|||
> Notice that some service methods may not be virtual, so you may not be able to override. We make all virtual by design. If you find any method that is not overridable, please [create an issue](https://github.com/abpframework/abp/issues/new) or do it yourself and send a **pull request** on GitHub. |
|||
|
|||
## Replacing an Interface |
|||
|
|||
If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityAppService`, you can re-implement the same interface and replace the current implementation by your class. Example: |
|||
|
|||
````csharp |
|||
public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
`MyIdentityUserAppService` replaces the `IIdentityUserAppService` by naming convention (since both ends with `IdentityUserAppService`). If your class name doesn't match, you need to manually expose the service interface: |
|||
|
|||
````csharp |
|||
[ExposeServices(typeof(IIdentityUserAppService))] |
|||
public class TestAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(IIdentityUserAppService))] |
|||
public class TestAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
In this way, there will be a single implementation of the `IIdentityUserAppService` interface, while it doesn't change the result for this case. Replacing a service is also possible by code: |
|||
|
|||
````csharp |
|||
context.Services.Replace( |
|||
ServiceDescriptor.Transient<IIdentityUserAppService, MyIdentityUserAppService>() |
|||
); |
|||
```` |
|||
|
|||
You can write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). |
|||
|
|||
## Overriding a Service Class |
|||
|
|||
In most cases, you will want to change one or a few methods of the current implementation for a service. Re-implementing the complete interface would not be efficient in this case. As a better approach, inherit from the original class and override the desired method. |
|||
|
|||
### Example: Overriding an Application Service |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
public class MyIdentityUserAppService : IdentityUserAppService |
|||
{ |
|||
//... |
|||
public MyIdentityUserAppService( |
|||
IdentityUserManager userManager, |
|||
IIdentityUserRepository userRepository, |
|||
IGuidGenerator guidGenerator |
|||
) : base( |
|||
userManager, |
|||
userRepository, |
|||
guidGenerator) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input) |
|||
{ |
|||
if (input.PhoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
throw new AbpValidationException( |
|||
"Phone number is required for new users!", |
|||
new List<ValidationResult> |
|||
{ |
|||
new ValidationResult( |
|||
"Phone number can not be empty!", |
|||
new []{"PhoneNumber"} |
|||
) |
|||
} |
|||
); } |
|||
|
|||
return await base.CreateAsync(input); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class **overrides** the `CreateAsync` method of the `IdentityUserAppService` [application service](Application-Services.md) to check the phone number. Then calls the base method to continue to the **underlying business logic**. In this way, you can perform additional business logic **before** and **after** the base logic. |
|||
|
|||
You could completely **re-write** the entire business logic for a user creation without calling the base method. |
|||
|
|||
### Example: Overriding a Domain Service |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(IdentityUserManager))] |
|||
public class MyIdentityUserManager : IdentityUserManager |
|||
{ |
|||
public MyIdentityUserManager( |
|||
IdentityUserStore store, |
|||
IOptions<IdentityOptions> optionsAccessor, |
|||
IPasswordHasher<IdentityUser> passwordHasher, |
|||
IEnumerable<IUserValidator<IdentityUser>> userValidators, |
|||
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, |
|||
ILookupNormalizer keyNormalizer, |
|||
IdentityErrorDescriber errors, |
|||
IServiceProvider services, |
|||
ILogger<IdentityUserManager> logger, |
|||
ICancellationTokenProvider cancellationTokenProvider |
|||
) : base( |
|||
store, |
|||
optionsAccessor, |
|||
passwordHasher, |
|||
userValidators, |
|||
passwordValidators, |
|||
keyNormalizer, |
|||
errors, |
|||
services, |
|||
logger, |
|||
cancellationTokenProvider) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<IdentityResult> CreateAsync(IdentityUser user) |
|||
{ |
|||
if (user.PhoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
throw new AbpValidationException( |
|||
"Phone number is required for new users!", |
|||
new List<ValidationResult> |
|||
{ |
|||
new ValidationResult( |
|||
"Phone number can not be empty!", |
|||
new []{"PhoneNumber"} |
|||
) |
|||
} |
|||
); |
|||
} |
|||
|
|||
return await base.CreateAsync(user); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example class inherits from the `IdentityUserManager` [domain service](Domain-Services.md) and overrides the `CreateAsync` method to perform the same phone number check implemented above. The result is same, but this time we've implemented it inside the domain service assuming that this is a **core domain logic** for our system. |
|||
|
|||
> `[ExposeServices(typeof(IdentityUserManager))]` attribute is **required** here since `IdentityUserManager` does not define an interface (like `IIdentityUserManager`) and dependency injection system doesn't expose services for inherited classes (like it does for the implemented interfaces) by convention. |
|||
|
|||
Check the [localization system](Localization.md) to learn how to localize the error messages. |
|||
|
|||
### Overriding Other Classes |
|||
|
|||
Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. |
|||
|
|||
## How to Find the Services? |
|||
|
|||
[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. |
|||
@ -0,0 +1,9 @@ |
|||
# Overriding the User Interface |
|||
|
|||
You may want to override a page, a component, a JavaScript, CSS or an image file of your depended module. Overriding the UI completely depends on the UI framework you're using. Select the UI framework to continue: |
|||
|
|||
* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) |
|||
* [Angular](UI/Angular/Customization-User-Interface.md) |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,912 @@ |
|||
# EF Core Database Migrations |
|||
|
|||
This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application. |
|||
|
|||
> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Index.md). |
|||
|
|||
### Source Code |
|||
|
|||
You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. |
|||
|
|||
## About the EF Core Code First Migrations |
|||
|
|||
Entity Framework Core provides an easy to use and powerful [database migration system](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). ABP Framework [startup templates](Startup-Templates/Index.md) take the advantage of this system to allow you to develop your application in a standard way. |
|||
|
|||
However, EF Core migration system is **not so good in a modular environment** where each module maintains its **own database schema** while two or more modules may **share a single database** in practical. |
|||
|
|||
Since ABP Framework cares about modularity in all aspects, it provides a **solution** to this problem. It is important to understand this solution if you need to **customize your database structure**. |
|||
|
|||
> See [EF Core's own documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to fully learn the EF Core Code First Migrations and why you need to such a system. |
|||
|
|||
## The Default Solution & Database Configuration |
|||
|
|||
When you [create a new web application](https://abp.io/get-started) (with EF Core, which is the default database provider), your solution structure will be similar to the picture below: |
|||
|
|||
 |
|||
|
|||
Actual solution structure may be a bit different based on your preferences, but the database part will be same. |
|||
|
|||
> This document will use the `Acme.BookStore` example project name to refer the projects and classes. You need to find the corresponding class/project in your solution. |
|||
|
|||
### The Database Structure |
|||
|
|||
The startup template has some [application modules](Modules/Index.md) pre-installed. Each layer of the solution has corresponding module **package references**. So, the `.EntityFrameworkCore` project has the NuGet references for the `.EntityFrameworkCore` packages of the used modules: |
|||
|
|||
 |
|||
|
|||
In this way, you collect all the **EF Core dependencies** under the `.EntityFrameworkCore` project. |
|||
|
|||
> In addition to the module references, it references to the `Volo.Abp.EntityFrameworkCore.SqlServer` package since the startup template is pre-configured for the **SQL Server**. See the documentation if you want to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
While every module has its own `DbContext` class by design and can use its **own physical database**, the solution is configured to use a **single shared database** as shown in the figure below: |
|||
|
|||
 |
|||
|
|||
This is **the simplest configuration** and suitable for most of the applications. `appsettings.json` file has a **single connection string**, named `Default`: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "..." |
|||
} |
|||
```` |
|||
|
|||
So, you have a **single database schema** which contains all the tables of the modules **sharing** this database. |
|||
|
|||
ABP Framework's [connection string](Connection-Strings.md) system allows you to easily **set a different connection string** for a desired module: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "...", |
|||
"AbpAuditLogging": "..." |
|||
} |
|||
```` |
|||
|
|||
The example configuration about tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md). |
|||
|
|||
**However, this is just the beginning**. You also need to create the second database, create audit log tables inside it and maintain the database tables using the code first migrations approach. One of the main purposes of this document is to guide you on such **database separation** scenarios. |
|||
|
|||
#### Module Tables |
|||
|
|||
Every module uses its **own databases tables**. For example, the [Identity Module](Modules/Identity.md) has some tables to manage the users and roles in the system. |
|||
|
|||
##### Table Prefixes |
|||
|
|||
Since it is allowed to share a single database by all modules (it is the default configuration), a module typically uses a **table name prefix** to group its own tables. |
|||
|
|||
The fundamental modules, like [Identity](Modules/Identity.md), [Tenant Management](Modules/Tenant-Management.md) and [Audit Logs](Modules/Audit-Logging.md), use the `Abp` prefix, while some other modules use their own prefixes. [Identity Server](Modules/IdentityServer.md) module uses the `IdentityServer` prefix for example. |
|||
|
|||
If you want, you can **change the database table name prefix** for a module for your application. Example: |
|||
|
|||
````csharp |
|||
Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; |
|||
```` |
|||
|
|||
This code changes the prefix of the [Identity Server](Modules/IdentityServer.md) module. Write this code **at the very beginning** in your application. |
|||
|
|||
> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage. |
|||
|
|||
### The Projects |
|||
|
|||
From the database point of view, there are three important projects those will be explained in the next sections. |
|||
|
|||
#### .EntityFrameworkCore Project |
|||
|
|||
This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application. |
|||
|
|||
**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your custom [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity: |
|||
|
|||
````csharp |
|||
[ConnectionStringName("Default")] |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<AppUser> Users { get; set; } |
|||
|
|||
/* Add DbSet properties for your Aggregate Roots / Entities here. */ |
|||
|
|||
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Configure the shared tables (with included modules) here */ |
|||
|
|||
builder.Entity<AppUser>(b => |
|||
{ |
|||
//Sharing the same table "AbpUsers" with the IdentityUser |
|||
b.ToTable("AbpUsers"); |
|||
|
|||
//Configure base properties |
|||
b.ConfigureByConvention(); |
|||
b.ConfigureAbpUser(); |
|||
|
|||
//Moved customization of the "AbpUsers" table to an extension method |
|||
b.ConfigureCustomUserProperties(); |
|||
}); |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
builder.ConfigureBookStore(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This simple `DbContext` class still needs some explanations: |
|||
|
|||
* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`. |
|||
* It inherits from the `AbpDbContext<T>` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext<T>` base class implements some conventions of the ABP Framework to automate some common tasks for you. |
|||
* It declares a `DbSet` property for the `AppUser` entity. `AppUser` shares the same table (named `AbpUsers` by default) with the `IdentityUser` entity of the [Identity module](Modules/Identity.md). The startup template provides this entity inside the application since we think that the User entity is generally needs to be customized in your application. |
|||
* The constructor takes a `DbContextOptions<T>` instance. |
|||
* It overrides the `OnModelCreating` method to define the EF Core mappings. |
|||
* It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us. |
|||
* It then configures the mapping for the `AppUser` entity. There is a special case for this entity (it shares a table with the Identity module), which will be explained in the next sections. |
|||
* It finally calls the `builder.ConfigureBookStore()` extension method to configure other entities of your application. |
|||
|
|||
This design will be explained in more details after introducing the other database related projects. |
|||
|
|||
#### .EntityFrameworkCore.DbMigrations Project |
|||
|
|||
As mentioned in the previous section, every module (and your application) have **their own** separate `DbContext` classes. Each `DbContext` class only defines the entity to table mappings related to its own module and each module (and your application) use the related `DbContext` class **on runtime**. |
|||
|
|||
As you know, EF Core Code First migration system relies on a `DbContext` class **to track and generate** the code first migrations. So, which `DbContext` we should use for the migrations? The answer is *none of them*. There is another `DbContext` defined in the `.EntityFrameworkCore.DbMigrations` project (which is the `BookStoreMigrationsDbContext` for this example solution). |
|||
|
|||
##### The MigrationsDbContext |
|||
|
|||
The `MigrationsDbContext` is only used to create and apply the database migrations. It is **not used on runtime**. It **merges** all the entity to table mappings of all the used modules plus the application's mappings. |
|||
|
|||
In this way, you create and maintain a **single database migration path**. However, there are some difficulties of this approach and the next sections explains how ABP Framework overcomes these difficulties. But first, see the `BookStoreMigrationsDbContext` class as an example: |
|||
|
|||
````csharp |
|||
/* This DbContext is only used for database migrations. |
|||
* It is not used on runtime. See BookStoreDbContext for the runtime DbContext. |
|||
* It is a unified model that includes configuration for |
|||
* all used modules and your application. |
|||
*/ |
|||
public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbContext> |
|||
{ |
|||
public BookStoreMigrationsDbContext( |
|||
DbContextOptions<BookStoreMigrationsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
builder.ConfigurePermissionManagement(); |
|||
builder.ConfigureSettingManagement(); |
|||
builder.ConfigureBackgroundJobs(); |
|||
builder.ConfigureAuditLogging(); |
|||
builder.ConfigureIdentity(); |
|||
builder.ConfigureIdentityServer(); |
|||
builder.ConfigureFeatureManagement(); |
|||
builder.ConfigureTenantManagement(); |
|||
|
|||
/* Configure customizations for entities from the modules included */ |
|||
builder.Entity<IdentityUser>(b => |
|||
{ |
|||
b.ConfigureCustomUserProperties(); |
|||
}); |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
builder.ConfigureBookStore(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
##### Sharing the Mapping Code |
|||
|
|||
First problem is that: A module uses its own `DbContext` which needs to the database mappings. The `MigrationsDbContext` also needs to the same mapping in order to create the database tables for this module. We definitely **don't want to duplicate** the mapping code. |
|||
|
|||
The solution is to define an **extension method** (on the `ModelBuilder`) that can be called by both `DbContext` classes. So, all modules define such extension methods. |
|||
|
|||
For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that: |
|||
|
|||
````csharp |
|||
public static class BackgroundJobsDbContextModelCreatingExtensions |
|||
{ |
|||
public static void ConfigureBackgroundJobs( |
|||
this ModelBuilder builder, |
|||
Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null) |
|||
{ |
|||
var options = new BackgroundJobsModelBuilderConfigurationOptions( |
|||
BackgroundJobsDbProperties.DbTablePrefix, |
|||
BackgroundJobsDbProperties.DbSchema |
|||
); |
|||
|
|||
optionsAction?.Invoke(options); |
|||
|
|||
builder.Entity<BackgroundJobRecord>(b => |
|||
{ |
|||
b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); |
|||
|
|||
b.ConfigureCreationTime(); |
|||
b.ConfigureExtraProperties(); |
|||
|
|||
b.Property(x => x.JobName) |
|||
.IsRequired() |
|||
.HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); |
|||
|
|||
//... |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This extension method also gets options to change the database table prefix and schema for this module, but it is not important here. |
|||
|
|||
The final application calls the extension methods inside the `MigrationsDbContext` class, so it can decide which modules are included in the database maintained by this `MigrationsDbContext`. If you want to create a second database and move some module tables to the second database, then you need to have a second `MigrationsDbContext` class which only calls the extension methods of the related modules. This topic will be detailed in the next sections. |
|||
|
|||
The same `ConfigureBackgroundJobs` method is also called in the `DbContext` of the Background Jobs module: |
|||
|
|||
````csharp |
|||
[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] |
|||
public class BackgroundJobsDbContext |
|||
: AbpDbContext<BackgroundJobsDbContext>, IBackgroundJobsDbContext |
|||
{ |
|||
public DbSet<BackgroundJobRecord> BackgroundJobs { get; set; } |
|||
|
|||
public BackgroundJobsDbContext(DbContextOptions<BackgroundJobsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
//Reuse the same extension method! |
|||
builder.ConfigureBackgroundJobs(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In this way, the mapping configuration of a module can be shared between `DbContext` classes. The code above is inside the related module NuGet package, so you don't care about it. |
|||
|
|||
##### Reusing a Table of a Module |
|||
|
|||
You may want to **reuse a table** of a depended module in your application. In this case, you have two options: |
|||
|
|||
1. You can **directly use the entity** defined by the module. |
|||
2. You can **create a new entity** mapping to the same database table. |
|||
|
|||
###### Use the Entity Defined by a Module |
|||
|
|||
Using an entity defined a module is pretty easy and standard. For example, Identity module defines the `IdentityUser` entity. You can inject the [repository](Repositories.md) for the `IdentityUser` and perform the standard repository operations for this entity. Example: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IRepository<IdentityUser, Guid> _identityUserRepository; |
|||
|
|||
public MyService(IRepository<IdentityUser, Guid> identityUserRepository) |
|||
{ |
|||
_identityUserRepository = identityUserRepository; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
//Get all users |
|||
var users = await _identityUserRepository.GetListAsync(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example injects the `IRepository<IdentityUser, Guid>` (default repository) which defines the standard repository methods and implements the `IQueryable` interface. |
|||
|
|||
> In addition, Identity module defines the `IIdentityUserRepository` (custom repository) that can also be injected and used by your application. `IIdentityUserRepository` provides additional custom methods for the `IdentityUser` entity while it does not implement the `IQueryable` interface. |
|||
|
|||
###### Create a New Entity |
|||
|
|||
Working with an entity of a module is easy if you want to use the entity as is. However, you may want to define your own entity class and map to the same database table in the following cases; |
|||
|
|||
* You want to **add a new field** to the table and map it to a property in the entity. You can't use the module's entity since it doesn't have the related property. |
|||
* You want to **use a subset of the table fields**. You don't want to access to all properties of the entity and hide the unrelated properties (from a security perspective or just by design). |
|||
* You don't want to directly **depend on** a module entity class. |
|||
|
|||
In any case, the progress is same. Assume that you want to create an entity, named `AppRole`, mapped to the same table of the `IdentityRole` entity of the [Identity module](Modules/Identity.md). |
|||
|
|||
Here, we will show the implementation, then **will discuss the limitations** of this approach. |
|||
|
|||
First, create a new `AppRole` class in your `.Domain` project: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Acme.BookStore.Roles |
|||
{ |
|||
public class AppRole : AggregateRoot<Guid>, IMultiTenant |
|||
{ |
|||
// Properties shared with the IdentityRole class |
|||
|
|||
public Guid? TenantId { get; private set; } |
|||
public string Name { get; private set; } |
|||
|
|||
//Additional properties |
|||
|
|||
public string Title { get; set; } |
|||
|
|||
private AppRole() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* It's inherited from [the `AggregateRoot<Guid>` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does the same. |
|||
* You can add any properties defined by the `IdentityRole` entity. This examples add only the `TenantId` and `Name` properties since we only need them here. You can make the setters private (like in this example) to prevent changing Identity module's properties accidently. |
|||
* You can add custom (additional) properties. This example adds the `Title` property. |
|||
* The **constructor is private**, so it is not allowed to directly create a new `AppRole` entity. Creating a role is a responsibility of the Identity module. You can query roles, set/update your custom properties, but you should not create or delete a role in your code, as a best practice (while there is nothing restricts you). |
|||
|
|||
Now, it is time to define the EF Core mappings. Open the `DbContext` of your application (`BookStoreDbContext` in this sample) and add the following property: |
|||
|
|||
````csharp |
|||
public DbSet<AppRole> Roles { get; set; } |
|||
```` |
|||
|
|||
Then configure the mapping inside the `OnModelCreating` method (after calling the `base.OnModelCreating(builder)`): |
|||
|
|||
````csharp |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Configure the shared tables (with included modules) here */ |
|||
|
|||
//CONFIGURE THE AppRole ENTITY |
|||
builder.Entity<AppRole>(b => |
|||
{ |
|||
b.ToTable("AbpRoles"); |
|||
|
|||
b.ConfigureByConvention(); |
|||
|
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
|
|||
... |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
|
|||
builder.ConfigureBookStore(); |
|||
} |
|||
```` |
|||
|
|||
We added the following lines: |
|||
|
|||
````csharp |
|||
builder.Entity<AppRole>(b => |
|||
{ |
|||
b.ToTable("AbpRoles"); |
|||
|
|||
b.ConfigureByConvention(); |
|||
|
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
```` |
|||
|
|||
* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. |
|||
* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it. |
|||
|
|||
`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project): |
|||
|
|||
````csharp |
|||
public static void ConfigureCustomRoleProperties<TRole>(this EntityTypeBuilder<TRole> b) |
|||
where TRole : class, IEntity<Guid> |
|||
{ |
|||
b.Property<string>(nameof(AppRole.Title)).HasMaxLength(128); |
|||
} |
|||
```` |
|||
|
|||
* This method only defines the **custom properties** of your entity. |
|||
* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table. |
|||
|
|||
You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`. |
|||
|
|||
Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below: |
|||
|
|||
````csharp |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
|
|||
... |
|||
|
|||
/* Configure customizations for entities from the modules included */ |
|||
|
|||
//CONFIGURE THE CUSTOM ROLE PROPERTIES |
|||
builder.Entity<IdentityRole>(b => |
|||
{ |
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
|
|||
... |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
|
|||
builder.ConfigureBookStore(); |
|||
} |
|||
```` |
|||
|
|||
Only added the following lines: |
|||
|
|||
````csharp |
|||
builder.Entity<IdentityRole>(b => |
|||
{ |
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
```` |
|||
|
|||
In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity. |
|||
|
|||
Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project): |
|||
|
|||
 |
|||
|
|||
This command will create a new code first migration class as shown below: |
|||
|
|||
````csharp |
|||
public partial class Added_Title_To_Roles : Migration |
|||
{ |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AddColumn<string>( |
|||
name: "Title", |
|||
table: "AbpRoles", |
|||
maxLength: 128, |
|||
nullable: true); |
|||
} |
|||
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "Title", |
|||
table: "AbpRoles"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
All done! Just run the `Update-Database` command in the PMC or run the `.DbMigrator` project in your solution to apply changes to database. |
|||
|
|||
Now, you can work with the `AppRole` entity just like any other entity of your application. An example [application service](Application-Services.md) that queries and updates roles: |
|||
|
|||
````csharp |
|||
public class AppRoleAppService : ApplicationService, IAppRoleAppService |
|||
{ |
|||
private readonly IRepository<AppRole, Guid> _appRoleRepository; |
|||
|
|||
public AppRoleAppService(IRepository<AppRole, Guid> appRoleRepository) |
|||
{ |
|||
_appRoleRepository = appRoleRepository; |
|||
} |
|||
|
|||
public async Task<List<AppRoleDto>> GetListAsync() |
|||
{ |
|||
var roles = await _appRoleRepository.GetListAsync(); |
|||
|
|||
return roles |
|||
.Select(r => new AppRoleDto |
|||
{ |
|||
Id = r.Id, |
|||
Name = r.Name, |
|||
Title = r.Title |
|||
}) |
|||
.ToList(); |
|||
} |
|||
|
|||
public async Task UpdateTitleAsync(Guid id, string title) |
|||
{ |
|||
var role = await _appRoleRepository.GetAsync(id); |
|||
|
|||
role.Title = title; |
|||
|
|||
await _appRoleRepository.UpdateAsync(role); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
There are some **limitations** of creating a new entity and mapping it to a table of a depended module: |
|||
|
|||
* Your **custom properties must be nullable**. For example, `AppRole.Title` was nullable here. Otherwise, Identity module throws exception because it doesn't know and can not fill the Title when it inserts a new role to the database. |
|||
* As a good practice, you should not update the **properties defined by the module**, especially if it requires a business logic. You typically want to manage your own properties. |
|||
|
|||
##### Alternative Approaches |
|||
|
|||
Instead of creating a new entity class to add a custom property, you can use the following approaches. |
|||
|
|||
###### Using the ExtraProperties |
|||
|
|||
All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property, which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity. |
|||
|
|||
For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example: |
|||
|
|||
````csharp |
|||
public class IdentityRoleExtendingService : ITransientDependency |
|||
{ |
|||
private readonly IIdentityRoleRepository _identityRoleRepository; |
|||
|
|||
public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) |
|||
{ |
|||
_identityRoleRepository = identityRoleRepository; |
|||
} |
|||
|
|||
public async Task<string> GetTitleAsync(Guid id) |
|||
{ |
|||
var role = await _identityRoleRepository.GetAsync(id); |
|||
|
|||
return role.GetProperty<string>("Title"); |
|||
} |
|||
|
|||
public async Task SetTitleAsync(Guid id, string newTitle) |
|||
{ |
|||
var role = await _identityRoleRepository.GetAsync(id); |
|||
|
|||
role.SetProperty("Title", newTitle); |
|||
|
|||
await _identityRoleRepository.UpdateAsync(role); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `GetProperty` and `SetProperty` methods are shortcuts to get and set a value in the `role.ExtraProperties` dictionary and they are the recommended way to work with the extra properties. |
|||
|
|||
In this way, you can easily attach any type of value to an entity of a depended module. However, there are some drawbacks of this usage: |
|||
|
|||
* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields. |
|||
* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors. |
|||
|
|||
###### Creating a New Table |
|||
|
|||
Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table. |
|||
|
|||
In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here). |
|||
|
|||
#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path |
|||
|
|||
As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. |
|||
|
|||
An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more module in the beginning, it has some important drawbacks: |
|||
|
|||
* **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code. |
|||
* It would be harder or impossible to **share a table** between modules or **re-use a table** of a module in your application. Because EF Core migration system can not handle it and will throw exceptions like "Table XXX is already exists in the database". |
|||
* It would be harder to **customize/enhance** the mapping and the resulting migration code. |
|||
* It would be harder to track and **apply changes** to database when you use multiple modules. |
|||
|
|||
## Using Multiple Databases |
|||
|
|||
The default startup template is organized to use a single database used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases. |
|||
|
|||
This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database. |
|||
|
|||
The resulting structure will be like the figure below: |
|||
|
|||
 |
|||
|
|||
### Change the Connection Strings Section |
|||
|
|||
First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" |
|||
} |
|||
```` |
|||
|
|||
Change it as shown below: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", |
|||
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", |
|||
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", |
|||
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" |
|||
} |
|||
```` |
|||
|
|||
Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all same). For example, `AbpPermissionManagement` is the connection string for the permission management module. |
|||
|
|||
The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string. |
|||
|
|||
### Create a Second Migration Project |
|||
|
|||
Defining the connection strings as explained above is enough **on runtime**. However, `BookStore_SecondDb` database doesn't exist yet. You need to create the database and the tables for the related modules. |
|||
|
|||
Just like the main database, we want to use the EF Core Code First migration system to create and maintain the second database. |
|||
|
|||
An easy way is to create a second project (`.csproj`) for the second migration `DbContext`. |
|||
|
|||
So, create a new **class library project** in your solution named `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` (or name it better if you didn't like it). |
|||
|
|||
The `.csproj` content should be something like that: |
|||
|
|||
````xml |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<RootNamespace>Acme.BookStore.DbMigrationsForSecondDb</RootNamespace> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Acme.BookStore.EntityFrameworkCore\Acme.BookStore.EntityFrameworkCore.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
```` |
|||
|
|||
You can just copy & modify the content of the original `.DbMigrations` project. This project references to the `.EntityFrameworkCore` project. **Only difference** is the `RootNamespace` value. |
|||
|
|||
**Add a reference** to this project from the `.Web` project (otherwise, EF Core tooling doesn't allow to use the `Add-Migration` command). |
|||
|
|||
### Create the Second DbMigrationDbContext |
|||
|
|||
Create a new `DbContext` for the migrations and call the **extension methods** of the modules to configure the database tables for the related modules: |
|||
|
|||
````csharp |
|||
[ConnectionStringName("AbpPermissionManagement")] |
|||
public class BookStoreSecondMigrationsDbContext : |
|||
AbpDbContext<BookStoreSecondMigrationsDbContext> |
|||
{ |
|||
public BookStoreSecondMigrationsDbContext( |
|||
DbContextOptions<BookStoreSecondMigrationsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
|
|||
builder.ConfigurePermissionManagement(); |
|||
builder.ConfigureSettingManagement(); |
|||
builder.ConfigureAuditLogging(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> `[ConnectionStringName(...)]` attribute is important here and tells to the ABP Framework which connection string should be used for this `DbContext`. We've used `AbpPermissionManagement`, but all are the same. |
|||
|
|||
Create a **Design Time Db Factory** class, that is used by the EF Core tooling (by `Add-Migration` and `Update-Database` PCM commands for example): |
|||
|
|||
````csharp |
|||
/* This class is needed for EF Core console commands |
|||
* (like Add-Migration and Update-Database commands) */ |
|||
public class BookStoreSecondMigrationsDbContextFactory |
|||
: IDesignTimeDbContextFactory<BookStoreSecondMigrationsDbContext> |
|||
{ |
|||
public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args) |
|||
{ |
|||
var configuration = BuildConfiguration(); |
|||
|
|||
var builder = new DbContextOptionsBuilder<BookStoreSecondMigrationsDbContext>() |
|||
.UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement")); |
|||
|
|||
return new BookStoreSecondMigrationsDbContext(builder.Options); |
|||
} |
|||
|
|||
private static IConfigurationRoot BuildConfiguration() |
|||
{ |
|||
var builder = new ConfigurationBuilder() |
|||
.SetBasePath(Directory.GetCurrentDirectory()) |
|||
.AddJsonFile("appsettings.json", optional: false); |
|||
|
|||
return builder.Build(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This is similar to the class inside the `.EntityFrameworCore.DbMigrations` project, except this one uses the `AbpPermissionManagement` connection string. |
|||
|
|||
Now, you can open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrationsForSecondDb` project as the default project (make sure the `.Web` project is still the startup project) and run the `Add-Migration "Initial"` and `Update-Database` commands as shown below: |
|||
|
|||
 |
|||
|
|||
Now, you should have a new database contains only the tables needed by the related modules: |
|||
|
|||
 |
|||
|
|||
### Remove Modules from the Main Database |
|||
|
|||
We've **created a second database** contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy. |
|||
|
|||
First, remove the following lines from the `MigrationsDbContext` class (`BookStoreMigrationsDbContext` for this example): |
|||
|
|||
````csharp |
|||
builder.ConfigurePermissionManagement(); |
|||
builder.ConfigureSettingManagement(); |
|||
builder.ConfigureAuditLogging(); |
|||
```` |
|||
|
|||
Open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrations` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command: |
|||
|
|||
```` |
|||
Add-Migration "Removed_Audit_Setting_Permission_Modules" |
|||
```` |
|||
|
|||
This command will create a new migration class as shown below: |
|||
|
|||
````csharp |
|||
public partial class Removed_Audit_Setting_Permission_Modules : Migration |
|||
{ |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "AbpAuditLogActions"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpEntityPropertyChanges"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpPermissionGrants"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpSettings"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpEntityChanges"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpAuditLogs"); |
|||
} |
|||
|
|||
... |
|||
} |
|||
```` |
|||
|
|||
Be careful in this step: |
|||
|
|||
* If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables. |
|||
* If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history. |
|||
|
|||
Run the `Update-Database` command to delete the tables from your main database. |
|||
|
|||
Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database. |
|||
|
|||
### Automate the Second Database Schema Migration |
|||
|
|||
`.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not run the EF Core Code First Migrations inside the second database migration project. Now, you will see how to configure the console migration application to handle both databases. |
|||
|
|||
#### Implementing the IBookStoreDbSchemaMigrator |
|||
|
|||
`EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that: |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
public class EntityFrameworkCoreBookStoreDbSchemaMigrator |
|||
: IBookStoreDbSchemaMigrator, ITransientDependency |
|||
{ |
|||
private readonly IServiceProvider _serviceProvider; |
|||
|
|||
public EntityFrameworkCoreBookStoreDbSchemaMigrator( |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
} |
|||
|
|||
public async Task MigrateAsync() |
|||
{ |
|||
/* We are intentionally resolving the BookStoreMigrationsDbContext |
|||
* from IServiceProvider (instead of directly injecting it) |
|||
* to properly get the connection string of the current tenant in the |
|||
* current scope. |
|||
*/ |
|||
|
|||
await _serviceProvider |
|||
.GetRequiredService<BookStoreMigrationsDbContext>() |
|||
.Database |
|||
.MigrateAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
It implements the `IBookStoreDbSchemaMigrator` and **replaces existing services** (see the first line). |
|||
|
|||
Remove the `[Dependency(ReplaceServices = true)]` line, because we will have two implementations of this interface and we want to use both. We don't want to replace one of them. |
|||
|
|||
Create a copy of this class inside the new migration project (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`), but use the `BookStoreSecondMigrationsDbContext`. Example implementation: |
|||
|
|||
````csharp |
|||
public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator |
|||
: IBookStoreDbSchemaMigrator, ITransientDependency |
|||
{ |
|||
private readonly IServiceProvider _serviceProvider; |
|||
|
|||
public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator( |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
} |
|||
|
|||
public async Task MigrateAsync() |
|||
{ |
|||
/* We are intentionally resolving the BookStoreSecondMigrationsDbContext |
|||
* from IServiceProvider (instead of directly injecting it) |
|||
* to properly get the connection string of the current tenant in the |
|||
* current scope. |
|||
*/ |
|||
|
|||
await _serviceProvider |
|||
.GetRequiredService<BookStoreSecondMigrationsDbContext>() |
|||
.Database |
|||
.MigrateAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> Name of this class is important for [dependency injection](Dependency-Injection.md). It should end with `BookStoreDbSchemaMigrator` to be injectable by `IBookStoreDbSchemaMigrator` reference. |
|||
|
|||
We, now, have two implementations of the `IBookStoreDbSchemaMigrator` interface, each one responsible to migrate the related database schema. |
|||
|
|||
#### Define a Module Class for the Second Migration Project |
|||
|
|||
It is time to define the [module](Module-Development-Basics.md) class for this second migrations (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`) project: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(BookStoreEntityFrameworkCoreModule) |
|||
)] |
|||
public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAbpDbContext<BookStoreSecondMigrationsDbContext>(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Now, reference `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.DbMigrator` project and `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreDbMigratorModule`. `BookStoreDbMigratorModule` class should be something like that: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpAutofacModule), |
|||
typeof(BookStoreEntityFrameworkCoreDbMigrationsModule), |
|||
typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS! |
|||
typeof(BookStoreApplicationContractsModule) |
|||
)] |
|||
public class BookStoreDbMigratorModule : AbpModule |
|||
{ |
|||
... |
|||
} |
|||
```` |
|||
|
|||
We had a reference to the `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.Web` project, but hadn't added module dependency since we hadn't created it before. But, now we have it and we need to add `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreWebModule` class. |
|||
|
|||
#### Run the Database Migrator! |
|||
|
|||
You can run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases. |
|||
|
|||
## Conclusion |
|||
|
|||
This document explains how to split your databases and manage your database migrations of your solution for Entity Framework Core. In brief, you need to have a separate migration project per different databases. |
|||
|
|||
## Source Code |
|||
|
|||
You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. |
|||
@ -0,0 +1,3 @@ |
|||
# Features |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# IdentityServer Integration |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# Account Module |
|||
|
|||
TODO |
|||
@ -1,6 +1,6 @@ |
|||
# Audit Logging Module |
|||
|
|||
The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. |
|||
The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. |
|||
|
|||
> Audit Logging module is already installed and configured for [the startup templates](../Startup-Templates/Index.md). So, most of the times you don't need to manually add this module to your application. |
|||
|
|||
|
|||
@ -1,3 +1,3 @@ |
|||
# IdentityServer Module |
|||
# Blogging Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# Feature Management Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,4 @@ |
|||
# Identity Management Module |
|||
|
|||
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
# Tenant Management Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
## Basic Theme |
|||
|
|||
TODO |
|||
@ -1,659 +1,6 @@ |
|||
## Angular Tutorial - Part I |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
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) |
|||
- [Part III: Integration Tests](Part-III.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: |
|||
|
|||
```js |
|||
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; |
|||
|
|||
//... |
|||
{ |
|||
path: 'books', |
|||
component: ApplicationLayoutComponent, |
|||
loadChildren: () => import('./books/books.module').then(m => m.BooksModule), |
|||
data: { |
|||
routes: { |
|||
name: 'Books', |
|||
} as ABP.Route, |
|||
}, |
|||
}, |
|||
``` |
|||
|
|||
`ApplicationLayoutComponent` 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: |
|||
|
|||
```js |
|||
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: |
|||
|
|||
```js |
|||
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: |
|||
|
|||
```js |
|||
// 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: |
|||
|
|||
```js |
|||
export namespace Books { |
|||
export interface State { |
|||
books: Response; |
|||
} |
|||
|
|||
export interface Response { |
|||
items: Book[]; |
|||
totalCount: number; |
|||
} |
|||
|
|||
export interface Book { |
|||
name: string; |
|||
type: BookType; |
|||
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 `books.service.ts` as shown below: |
|||
|
|||
```js |
|||
import { Injectable } from '@angular/core'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { Books } from '../../store/models'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BooksService { |
|||
constructor(private restService: RestService) {} |
|||
|
|||
get(): Observable<Books.Response> { |
|||
return this.restService.request<void, Books.Response>({ |
|||
method: 'GET', |
|||
url: '/api/app/book' |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
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: |
|||
|
|||
```js |
|||
export class GetBooks { |
|||
static readonly type = '[Books] Get'; |
|||
} |
|||
``` |
|||
|
|||
#### Implement the BooksState |
|||
|
|||
Open the `books.state.ts` and change the file as shown below: |
|||
|
|||
```js |
|||
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(state: Books.State) { |
|||
return state.books.items || []; |
|||
} |
|||
|
|||
constructor(private booksService: BooksService) {} |
|||
|
|||
@Action(GetBooks) |
|||
get(ctx: StateContext<Books.State>) { |
|||
return this.booksService.get().pipe( |
|||
tap(booksResponse => { |
|||
ctx.patchState({ |
|||
books: booksResponse, |
|||
}); |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
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: |
|||
|
|||
```js |
|||
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. |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
@ -1,587 +1,6 @@ |
|||
## Angular Tutorial - Part II |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
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)** |
|||
- [Part III: Integration Tests](Part-III.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 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: |
|||
|
|||
```js |
|||
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: |
|||
|
|||
```js |
|||
create(createBookInput: Books.CreateUpdateBookInput): Observable<Books.Book> { |
|||
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({ |
|||
method: 'POST', |
|||
url: '/api/app/book', |
|||
body: createBookInput |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). |
|||
|
|||
#### State Definitions |
|||
|
|||
Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: |
|||
|
|||
```js |
|||
import { Books } from '../models'; |
|||
|
|||
export class CreateUpdateBook { |
|||
static readonly type = '[Books] Create Update Book'; |
|||
constructor(public payload: Books.CreateUpdateBookInput) {} |
|||
} |
|||
``` |
|||
|
|||
Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book: |
|||
|
|||
```js |
|||
import { ... , CreateUpdateBook } from '../actions/books.actions'; |
|||
import { ... , switchMap } from 'rxjs/operators'; |
|||
//... |
|||
@Action(CreateUpdateBook) |
|||
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { |
|||
return this.booksService |
|||
.create(action.payload) |
|||
.pipe(switchMap(() => ctx.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 modal to create a new book. |
|||
|
|||
```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)="createBook()"> |
|||
<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 `createBook` method to show/hide the modal. |
|||
|
|||
```js |
|||
isModalOpen = false; |
|||
|
|||
//... |
|||
|
|||
createBook() { |
|||
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). |
|||
|
|||
```js |
|||
import { FormGroup, FormBuilder, Validators } 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. |
|||
|
|||
```js |
|||
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` (`fb`) creates a `FormGroup`. |
|||
- Added `Validators.required` static method that validates the related form element. |
|||
|
|||
Modify the `createBook` method as shown below: |
|||
|
|||
```js |
|||
createBook() { |
|||
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> |
|||
``` |
|||
|
|||
- This template creates a form with Name, Price, Type and Publish date fields. |
|||
|
|||
> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. |
|||
|
|||
#### Datepicker Requirements |
|||
|
|||
You need to import `NgbDatepickerModule` to the `books.module.ts`: |
|||
|
|||
```js |
|||
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// ... |
|||
NgbDatepickerModule, |
|||
], |
|||
}) |
|||
export class BooksModule {} |
|||
``` |
|||
|
|||
Then open the `book-list.component.ts` and add `providers` as shown below: |
|||
|
|||
```js |
|||
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Component({ |
|||
// ... |
|||
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], |
|||
}) |
|||
export class BookListComponent implements OnInit { |
|||
// ... |
|||
``` |
|||
|
|||
> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. |
|||
|
|||
#### Create the Book Type Array |
|||
|
|||
Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: |
|||
|
|||
```js |
|||
//... |
|||
booksType = Books.BookType; |
|||
|
|||
bookTypeArr = Object.keys(Books.BookType).filter( |
|||
bookType => typeof this.booksType[bookType] === 'number' |
|||
); |
|||
``` |
|||
|
|||
The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: |
|||
|
|||
```js |
|||
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] |
|||
``` |
|||
|
|||
This array was used in the previous form template (in the `ngFor` loop). |
|||
|
|||
|
|||
 |
|||
|
|||
#### Saving the Book |
|||
|
|||
Open the `book-list.component.html` and add an `abp-button` to save the form. |
|||
|
|||
```html |
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
Cancel |
|||
</button> |
|||
<button class="btn btn-primary" (click)="save()"> |
|||
<i class="fa fa-check mr-1"></i> |
|||
Save |
|||
</button> |
|||
</ng-template> |
|||
``` |
|||
|
|||
This adds a save button to the bottom area of the modal: |
|||
|
|||
 |
|||
|
|||
Then define a `save` method in the `BookListComponent`: |
|||
|
|||
```js |
|||
//... |
|||
import { ..., CreateUpdateBook } from '../../store/actions'; |
|||
//... |
|||
save() { |
|||
if (this.form.invalid) { |
|||
return; |
|||
} |
|||
|
|||
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { |
|||
this.isModalOpen = false; |
|||
this.form.reset(); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### Updating An Existing Book |
|||
|
|||
#### BooksService |
|||
|
|||
Open the `books.service.ts` and then add the `getById` and `update` methods. |
|||
|
|||
```js |
|||
getById(id: string): Observable<Books.Book> { |
|||
return this.restService.request<void, Books.Book>({ |
|||
method: 'GET', |
|||
url: `/api/app/book/${id}` |
|||
}); |
|||
} |
|||
|
|||
update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { |
|||
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({ |
|||
method: 'PUT', |
|||
url: `/api/app/book/${id}`, |
|||
body: updateBookInput |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
#### CreateUpdateBook Action |
|||
|
|||
Open the `books.actions.ts` and add `id` parameter to the `CreateUpdateBook` action: |
|||
|
|||
```js |
|||
export class CreateUpdateBook { |
|||
static readonly type = '[Books] Create Update Book'; |
|||
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} |
|||
} |
|||
``` |
|||
|
|||
Open `books.state.ts` and modify the `save` method as show below: |
|||
|
|||
```js |
|||
@Action(CreateUpdateBook) |
|||
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { |
|||
let request; |
|||
|
|||
if (action.id) { |
|||
request = this.booksService.update(action.payload, action.id); |
|||
} else { |
|||
request = this.booksService.create(action.payload); |
|||
} |
|||
|
|||
return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); |
|||
} |
|||
``` |
|||
|
|||
#### BookListComponent |
|||
|
|||
Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. |
|||
|
|||
```js |
|||
import { BooksService } from '../shared/books.service'; |
|||
//... |
|||
selectedBook = {} as Books.Book; |
|||
|
|||
constructor( |
|||
//... |
|||
private booksService: BooksService |
|||
) |
|||
``` |
|||
|
|||
`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. |
|||
|
|||
```js |
|||
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 `editBook` method as shown below: |
|||
|
|||
```js |
|||
editBook(id: string) { |
|||
this.booksService.getById(id).subscribe(book => { |
|||
this.selectedBook = book; |
|||
this.buildForm(); |
|||
this.isModalOpen = true; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Added `editBook` method to get the editing book, build the form and show the modal. |
|||
|
|||
Now, add the `selectedBook` definition to `createBook` method to reuse the same form while creating a new book: |
|||
|
|||
```js |
|||
createBook() { |
|||
this.selectedBook = {} as Books.Book; |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
Modify the `save` method to pass the id of the selected book as shown below: |
|||
|
|||
```js |
|||
save() { |
|||
if (this.form.invalid) { |
|||
return; |
|||
} |
|||
|
|||
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) |
|||
.subscribe(() => { |
|||
this.isModalOpen = false; |
|||
this.form.reset(); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
#### 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)="editBook(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> |
|||
``` |
|||
|
|||
- Added a `th` for the "Actions" column. |
|||
- Added `button` with `ngbDropdownToggle` to open actions when clicked the button. |
|||
|
|||
> We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. |
|||
|
|||
The final UI looks like: |
|||
|
|||
 |
|||
|
|||
Update the modal header to change the title based on the current operation: |
|||
|
|||
```html |
|||
<ng-template #abpHeader> |
|||
<h3>{%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}</h3> |
|||
</ng-template> |
|||
``` |
|||
|
|||
 |
|||
|
|||
### Deleting an Existing Book |
|||
|
|||
#### BooksService |
|||
|
|||
Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: |
|||
|
|||
```js |
|||
delete(id: string): Observable<void> { |
|||
return this.restService.request<void, void>({ |
|||
method: 'DELETE', |
|||
url: `/api/app/book/${id}` |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
#### DeleteBook Action |
|||
|
|||
Add an action named `DeleteBook` to `books.actions.ts`: |
|||
|
|||
```js |
|||
export class DeleteBook { |
|||
static readonly type = '[Books] Delete'; |
|||
constructor(public id: string) {} |
|||
} |
|||
``` |
|||
|
|||
Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: |
|||
|
|||
```js |
|||
import { ... , DeleteBook } from '../actions/books.actions'; |
|||
//... |
|||
@Action(DeleteBook) |
|||
delete(ctx: StateContext<Books.State>, action: DeleteBook) { |
|||
return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); |
|||
} |
|||
``` |
|||
|
|||
- Added `DeleteBook` to the import list. |
|||
- Uses `bookService` to delete the book. |
|||
|
|||
#### Add a Delete Button |
|||
|
|||
Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: |
|||
|
|||
```html |
|||
<div ngbDropdownMenu> |
|||
... |
|||
<button ngbDropdownItem (click)="delete(data.id, data.name)"> |
|||
Delete |
|||
</button> |
|||
</div> |
|||
``` |
|||
|
|||
The final actions dropdown UI looks like below: |
|||
|
|||
 |
|||
|
|||
#### Delete Confirmation Dialog |
|||
|
|||
Open `book-list.component.ts` and inject the `ConfirmationService`. |
|||
|
|||
```js |
|||
import { ConfirmationService } from '@abp/ng.theme.shared'; |
|||
//... |
|||
constructor( |
|||
//... |
|||
private confirmationService: ConfirmationService |
|||
) |
|||
``` |
|||
|
|||
> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. |
|||
|
|||
Add a delete method to the `BookListComponent`: |
|||
|
|||
```js |
|||
import { ... , DeleteBook } from '../../store/actions'; |
|||
import { ... , 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 a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: |
|||
|
|||
 |
|||
|
|||
### Next Part |
|||
|
|||
See the [next part](Part-III.md) of this tutorial. |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
@ -1,178 +1,6 @@ |
|||
## Angular Tutorial - Part III |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
This is the third 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](Part-II.md) |
|||
- **Part III: Integration Tests (this tutorial)** |
|||
|
|||
This part covers the **server side** tests. 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). |
|||
|
|||
### Test Projects in the Solution |
|||
|
|||
There are multiple test projects in the solution: |
|||
|
|||
 |
|||
|
|||
Each project is used to test the related application project. Test projects use the following libraries for testing: |
|||
|
|||
* [xunit](https://xunit.github.io/) as the main test framework. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. |
|||
* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. |
|||
|
|||
### Adding Test Data |
|||
|
|||
Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. |
|||
|
|||
Change the `BookStoreTestDataSeedContributor` class as show below: |
|||
|
|||
````C# |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Guids; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreTestDataSeedContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IGuidGenerator _guidGenerator; |
|||
|
|||
public BookStoreTestDataSeedContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IGuidGenerator guidGenerator) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_guidGenerator = guidGenerator; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Id = _guidGenerator.Create(), |
|||
Name = "Test book 1", |
|||
Type = BookType.Fantastic, |
|||
PublishDate = new DateTime(2015, 05, 24), |
|||
Price = 21 |
|||
} |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book |
|||
{ |
|||
Id = _guidGenerator.Create(), |
|||
Name = "Test book 2", |
|||
Type = BookType.Science, |
|||
PublishDate = new DateTime(2014, 02, 11), |
|||
Price = 15 |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Injected `IRepository<Book, Guid>` and used it in the `SeedAsync` to create two book entities as the test data. |
|||
* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). |
|||
|
|||
### Testing the BookAppService |
|||
|
|||
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. |
|||
|
|||
Add a new test that creates a valid new book: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
```` |
|||
|
|||
Add a new test that tries to create an invalid book and fails: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
```` |
|||
|
|||
* Since the `Name` is empty, ABP throws an `AbpValidationException`. |
|||
|
|||
Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: |
|||
|
|||
 |
|||
|
|||
Congratulations, green icons show that tests have been successfully passed! |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@ -1,476 +1,6 @@ |
|||
## ASP.NET Core MVC Tutorial - Part I |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Entity Framework Core** (EF Core) will be used as the ORM provider as it is the default database provider. |
|||
|
|||
This is the first part of the ASP.NET Core MVC 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) |
|||
- [Part III: Integration Tests](Part-III.md) |
|||
|
|||
You can access to the **source code** of the application from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/BookStore). |
|||
|
|||
> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. |
|||
|
|||
### Creating the Project |
|||
|
|||
Create a new project named `Acme.BookStore`, create the database and run the application by following the [Getting Started document](../../Getting-Started-AspNetCore-MVC-Template.md). |
|||
|
|||
### Solution Structure |
|||
|
|||
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; } |
|||
|
|||
protected Book() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Book(Guid id, string name, BookType type, DateTime publishDate, float price) |
|||
:base(id) |
|||
{ |
|||
Name = name; |
|||
Type = type; |
|||
PublishDate = publishDate; |
|||
Price = price; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* 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 |
|||
|
|||
EF Core requires you to relate entities with your DbContext. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: |
|||
|
|||
````C# |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<Book> Books { get; set; } |
|||
... |
|||
} |
|||
```` |
|||
|
|||
#### Configure Your Book Entity |
|||
|
|||
Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity: |
|||
|
|||
````C# |
|||
builder.Entity<Book>(b => |
|||
{ |
|||
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); |
|||
b.ConfigureByConvention(); //auto configure for the base class props |
|||
b.Property(x => x.Name).IsRequired().HasMaxLength(128); |
|||
}); |
|||
```` |
|||
|
|||
#### Add New Migration & Update the Database |
|||
|
|||
The Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** (under the *Tools/Nuget Package Manager* menu), select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command: |
|||
|
|||
 |
|||
|
|||
This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema: |
|||
|
|||
```` |
|||
PM> Update-Database |
|||
```` |
|||
|
|||
#### Add Sample Data |
|||
|
|||
`Update-Database` command created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the page: |
|||
|
|||
 |
|||
|
|||
### 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 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. |
|||
|
|||
### Dynamic JavaScript Proxies |
|||
|
|||
It's common to call HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. |
|||
|
|||
ABP **dynamically** creates JavaScript **proxies** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. |
|||
|
|||
#### Testing in the Browser Developer Console |
|||
|
|||
You can easily test the JavaScript proxies using your favorite browser's **Developer Console** now. Run the application, open your browser's **developer tools** (shortcut: F12), switch to the **Console** tab, type the following code and press enter: |
|||
|
|||
````js |
|||
acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); |
|||
```` |
|||
|
|||
* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). |
|||
* `book` is the conventional name for the `BookAppService` (removed AppService postfix and converted to camelCase). |
|||
* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed Async postfix and converted to camelCase). |
|||
* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object). |
|||
* `getList` function returns a `promise`. So, you can pass a callback to the `done` (or `then`) function to get the result from the server. |
|||
|
|||
Running this code produces the following output: |
|||
|
|||
 |
|||
|
|||
You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: |
|||
|
|||
 |
|||
|
|||
Let's **create a new book** using the `create` function: |
|||
|
|||
````js |
|||
acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); |
|||
```` |
|||
|
|||
You should see a message in the console something like that: |
|||
|
|||
```` |
|||
successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 |
|||
```` |
|||
|
|||
Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. |
|||
|
|||
### Create the Books Page |
|||
|
|||
It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. |
|||
|
|||
Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.cshtml`: |
|||
|
|||
 |
|||
|
|||
Open the `Index.cshtml` and change the content as shown below: |
|||
|
|||
````html |
|||
@page |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model IndexModel |
|||
|
|||
<h2>Books</h2> |
|||
```` |
|||
|
|||
* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template and provides some shared properties/methods used by all pages. |
|||
* Ensure that the `IndexModel` (*Index.cshtml.cs)* has the `Acme.BookStore.Web.Pages.Books` namespace, or update it in the `Index.cshtml`. |
|||
|
|||
#### Add Books Page to the Main Menu |
|||
|
|||
Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: |
|||
|
|||
````c# |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) |
|||
.AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) |
|||
); |
|||
```` |
|||
|
|||
#### Localizing the Menu Items |
|||
|
|||
Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: |
|||
|
|||
 |
|||
|
|||
Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys to the end of the file: |
|||
|
|||
````json |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Menu:BookStore": "Book Store", |
|||
"Menu:Books": "Books" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](../../Localization.md) for details. |
|||
* Localization key names are arbitrary. You can set any name. We prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (ASP.NET Core's standard behavior). |
|||
|
|||
Run the application and see the new menu item has been added to the top bar: |
|||
|
|||
 |
|||
|
|||
When you click to the Books menu item, you are redirected to the new Books page. |
|||
|
|||
#### Book List |
|||
|
|||
We will use the [Datatables.net](https://datatables.net/) JQuery plugin to show list of tables on the page. Datatables can completely work via AJAX, it is fast and provides a good user experience. Datatables plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page. |
|||
|
|||
##### Index.cshtml |
|||
|
|||
Change the `Pages/Books/Index.cshtml` as following: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@model Acme.BookStore.Web.Pages.Books.IndexModel |
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/index.js" /> |
|||
} |
|||
<abp-card> |
|||
<abp-card-header> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
```` |
|||
|
|||
* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning** for example. See the [bundling & minification document](../../AspNetCore/Bundling-Minification.md) for details. |
|||
* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are many tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help of the intellisense and compile time type checking. See the [tag helpers document](../../AspNetCore/Tag-Helpers/Index.md). |
|||
* You can **localize** the column names in the localization file as you did for the menu items above. |
|||
|
|||
##### Add a Script File |
|||
|
|||
Create `index.js` JavaScript file under the `Pages/Books/` folder: |
|||
|
|||
 |
|||
|
|||
`index.js` content is shown below: |
|||
|
|||
````js |
|||
$(function () { |
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
}); |
|||
```` |
|||
|
|||
* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to Datatable's format. |
|||
* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the datatables configuration by providing conventional values for missing options. |
|||
* `acme.bookStore.book.getList` is the function to get list of books (you have seen it before). |
|||
* See [Datatable's documentation](https://datatables.net/manual/) for more configuration options. |
|||
|
|||
The final UI is shown below: |
|||
|
|||
 |
|||
|
|||
### Next Part |
|||
|
|||
See the [next part](Part-II.md) of this tutorial. |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
@ -1,432 +1,6 @@ |
|||
## ASP.NET Core MVC Tutorial - Part II |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
This is the second part of the ASP.NET Core MVC 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)** |
|||
* [Part III: Integration Tests](Part-III.md) |
|||
|
|||
You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). |
|||
|
|||
> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. |
|||
|
|||
### Creating a New Book |
|||
|
|||
In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that: |
|||
|
|||
 |
|||
|
|||
#### Create the Modal Form |
|||
|
|||
Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: |
|||
|
|||
 |
|||
|
|||
##### CreateModal.cshtml.cs |
|||
|
|||
Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class CreateModalModel : BookStorePageModel |
|||
{ |
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public CreateModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.CreateAsync(Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties/methods those can be used by your page model classes. |
|||
* `[BindProperty]` attribute on the `Book` property binds post request data to this property. |
|||
* This class simply injects the `IBookAppService` in its constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. |
|||
|
|||
##### CreateModal.cshtml |
|||
|
|||
Open the `CreateModal.cshtml` file and paste the code below: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model Acme.BookStore.Web.Pages.Books.CreateModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewBook"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the `CreateBookViewModel` class. |
|||
* `abp-model` attribute indicates the model object, the `Book` property in this case. |
|||
* `data-ajaxForm` attribute makes the form submitting via AJAX, instead of a classic page post. |
|||
* `abp-form-content` tag helper is a placeholder to render the form controls (this is optional and needed only if you added some other content in the `abp-dynamic-form` tag, just like in this page). |
|||
|
|||
#### Add the "New book" Button |
|||
|
|||
Open the `Pages/Books/Index.cshtml` and change the `abp-card-header` tag as shown below: |
|||
|
|||
````html |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<h2>@L["Books"]</h2> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-right"> |
|||
<abp-button id="NewBookButton" |
|||
text="@L["NewBook"].Value" |
|||
icon="plus" |
|||
button-type="Primary" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
```` |
|||
|
|||
Just added a **New book** button to the **top right** of the table: |
|||
|
|||
 |
|||
|
|||
Open the `pages/books/index.js` and add the following code just after the datatable configuration: |
|||
|
|||
````js |
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
```` |
|||
|
|||
* `abp.ModalManager` is a helper class to open and manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. |
|||
|
|||
Now, you can **run the application** and add new books using the new modal form. |
|||
|
|||
### Updating An Existing Book |
|||
|
|||
Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: |
|||
|
|||
 |
|||
|
|||
#### EditModal.cshtml.cs |
|||
|
|||
Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books |
|||
{ |
|||
public class EditModalModel : BookStorePageModel |
|||
{ |
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[BindProperty] |
|||
public CreateUpdateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public EditModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
var bookDto = await _bookAppService.GetAsync(Id); |
|||
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto); |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.UpdateAsync(Id, Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. Used `SupportsGet` to be able to get Id value from query string parameter of the request. |
|||
* Mapped `BookDto` (received from the `BookAppService.GetAsync`) to `CreateUpdateBookDto` in the `GetAsync` method. |
|||
* The `OnPostAsync` simply uses `BookAppService.UpdateAsync` to update the entity. |
|||
|
|||
#### BookDto to CreateUpdateBookDto Mapping |
|||
|
|||
In order to perform `BookDto` to `CreateUpdateBookDto` object mapping, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below: |
|||
|
|||
````csharp |
|||
using AutoMapper; |
|||
|
|||
namespace Acme.BookStore.Web |
|||
{ |
|||
public class BookStoreWebAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreWebAutoMapperProfile() |
|||
{ |
|||
CreateMap<BookDto, CreateUpdateBookDto>(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Just added `CreateMap<BookDto, CreateUpdateBookDto>();` as the mapping definition. |
|||
|
|||
#### EditModal.cshtml |
|||
|
|||
Replace `EditModal.cshtml` content with the following content: |
|||
|
|||
````html |
|||
@page |
|||
@inherits Acme.BookStore.Web.Pages.BookStorePage |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model EditModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" data-ajaxForm="true" asp-page="/Books/EditModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["Update"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-input asp-for="Id" /> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
```` |
|||
|
|||
This page is very similar to the `CreateModal.cshtml` except; |
|||
|
|||
* It includes an `abp-input` for the `Id` property to store id of the editing book (which is a hidden input). |
|||
* It uses `Books/EditModal` as the post URL and *Update* text as the modal header. |
|||
|
|||
#### Add "Actions" Dropdown to the Table |
|||
|
|||
We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this: |
|||
|
|||
 |
|||
|
|||
Open the `Pages/Books/Index.cshtml` page and change the table section as shown below: |
|||
|
|||
````html |
|||
<abp-table striped-rows="true" id="BooksTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Actions"]</th> |
|||
<th>@L["Name"]</th> |
|||
<th>@L["Type"]</th> |
|||
<th>@L["PublishDate"]</th> |
|||
<th>@L["Price"]</th> |
|||
<th>@L["CreationTime"]</th> |
|||
</tr> |
|||
</thead> |
|||
</abp-table> |
|||
```` |
|||
|
|||
* Just added a new `th` tag for the "Actions". |
|||
|
|||
Open the `pages/books/index.js` and replace the content as below: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server side. |
|||
* Added a new `ModalManager` named `createModal` to open the create modal dialog. |
|||
* Added a new `ModalManager` named `editModal` to open the edit modal dialog. |
|||
* Added a new column at the beginning of the `columnDefs` section. This column is used for the "Actions" dropdown button. |
|||
* "New Book" action simply calls `createModal.open` to open the create dialog. |
|||
* "Edit" action simply calls `editModal.open` to open the edit dialog. |
|||
` |
|||
You can run the application and edit any book by selecting the edit action. |
|||
|
|||
### Deleting an Existing Book |
|||
|
|||
Open the `pages/books/index.js` and add a new item to the `rowAction` `items`: |
|||
|
|||
````js |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `confirmMessage` option is used to ask a confirmation question before executing the `action`. |
|||
* Used `acme.bookStore.book.delete` javascript proxy function to perform an AJAX request to delete a book. |
|||
* `abp.notify.info` is used to show a toastr notification just after the deletion. |
|||
|
|||
The final `index.js` content is shown below: |
|||
|
|||
````js |
|||
$(function () { |
|||
|
|||
var l = abp.localization.getResource('BookStore'); |
|||
|
|||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); |
|||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); |
|||
|
|||
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ |
|||
processing: true, |
|||
serverSide: true, |
|||
paging: true, |
|||
searching: false, |
|||
autoWidth: false, |
|||
scrollCollapse: true, |
|||
order: [[1, "asc"]], |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
}, |
|||
{ |
|||
text: l('Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('BookDeletionConfirmationMessage', data.record.name); |
|||
}, |
|||
action: function (data) { |
|||
acme.bookStore.book |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ data: "name" }, |
|||
{ data: "type" }, |
|||
{ data: "publishDate" }, |
|||
{ data: "price" }, |
|||
{ data: "creationTime" } |
|||
] |
|||
})); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following line: |
|||
|
|||
````json |
|||
"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", |
|||
"SuccessfullyDeleted": "Successfully deleted" |
|||
```` |
|||
|
|||
Run the application and try to delete a book. |
|||
|
|||
### Next Part |
|||
|
|||
See the [next part](Part-III.md) of this tutorial. |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
@ -1,166 +1,6 @@ |
|||
## ASP.NET Core MVC Tutorial - Part III |
|||
# Tutorials |
|||
|
|||
### About this Tutorial |
|||
## Application Development |
|||
|
|||
This is the third part of the ASP.NET Core MVC 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](Part-II.md) |
|||
- **Part III: Integration Tests (this tutorial)** |
|||
|
|||
You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). |
|||
|
|||
> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. |
|||
|
|||
### Test Projects in the Solution |
|||
|
|||
There are multiple test projects in the solution: |
|||
|
|||
 |
|||
|
|||
Each project is used to test the related application project. Test projects use the following libraries for testing: |
|||
|
|||
* [xunit](https://xunit.github.io/) as the main test framework. |
|||
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. |
|||
* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. |
|||
|
|||
### Adding Test Data |
|||
|
|||
Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. |
|||
|
|||
Change the `BookStoreTestDataSeedContributor` class as show below: |
|||
|
|||
````C# |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Guids; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookStoreTestDataSeedContributor |
|||
: IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Book, Guid> _bookRepository; |
|||
private readonly IGuidGenerator _guidGenerator; |
|||
|
|||
public BookStoreTestDataSeedContributor( |
|||
IRepository<Book, Guid> bookRepository, |
|||
IGuidGenerator guidGenerator) |
|||
{ |
|||
_bookRepository = bookRepository; |
|||
_guidGenerator = guidGenerator; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
await _bookRepository.InsertAsync( |
|||
new Book(_guidGenerator.Create(), "Test book 1", BookType.Fantastic, new DateTime(2015, 05, 24), 21) |
|||
); |
|||
|
|||
await _bookRepository.InsertAsync( |
|||
new Book(_guidGenerator.Create(), "Test book 2", BookType.Science, new DateTime(2014, 02, 11), 15) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Injected `IRepository<Book, Guid>` and used it in the `SeedAsync` to create two book entities as the test data. |
|||
* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). |
|||
|
|||
### Testing the BookAppService |
|||
|
|||
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: |
|||
|
|||
````C# |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Xunit; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class BookAppService_Tests : BookStoreApplicationTestBase |
|||
{ |
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public BookAppService_Tests() |
|||
{ |
|||
_bookAppService = GetRequiredService<IBookAppService>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List_Of_Books() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.GetListAsync( |
|||
new PagedAndSortedResultRequestDto() |
|||
); |
|||
|
|||
//Assert |
|||
result.TotalCount.ShouldBeGreaterThan(0); |
|||
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. |
|||
|
|||
Add a new test that creates a valid new book: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Create_A_Valid_Book() |
|||
{ |
|||
//Act |
|||
var result = await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "New test book 42", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
|
|||
//Assert |
|||
result.Id.ShouldNotBe(Guid.Empty); |
|||
result.Name.ShouldBe("New test book 42"); |
|||
} |
|||
```` |
|||
|
|||
Add a new test that tries to create an invalid book and fails: |
|||
|
|||
````C# |
|||
[Fact] |
|||
public async Task Should_Not_Create_A_Book_Without_Name() |
|||
{ |
|||
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () => |
|||
{ |
|||
await _bookAppService.CreateAsync( |
|||
new CreateUpdateBookDto |
|||
{ |
|||
Name = "", |
|||
Price = 10, |
|||
PublishDate = DateTime.Now, |
|||
Type = BookType.ScienceFiction |
|||
} |
|||
); |
|||
}); |
|||
|
|||
exception.ValidationErrors |
|||
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
|||
} |
|||
```` |
|||
|
|||
* Since the `Name` is empty, ABP throws an `AbpValidationException`. |
|||
|
|||
Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: |
|||
|
|||
 |
|||
|
|||
Congratulations, green icons show that tests have been successfully passed! |
|||
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) |
|||
* [With Angular UI](../Part-1?UI=NG) |
|||
|
|||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |