12 KiB
Introducing the Angular Service Proxy Generation
Angular Service Proxy System generates TypeScript services and models to consume your backend HTTP APIs developed using the ABP Framework. So, you don't manually create models for your server side DTOs and perform raw HTTP calls to the server.
ABP Framework has introduced the new Angular Service Proxy Generation system with the version 3.1. While this feature was available since the v2.3, it was not well covering some scenarios, like inheritance and generic types and had some known problems. With the v3.1, we've re-written it using the Angular Schematics system. Now, it is much more stable and feature rich.
This post introduces the service proxy generation system and highlights some important features.
Installation
ABP CLI
You need to have the ABP CLI to use the system. So, install it if you haven't installed before:
dotnet tool install -g Volo.Abp.Cli
If you already have installed it before, you can update to the latest version:
dotnet tool update -g Volo.Abp.Cli
Project Configuration
If you've created your project with version 3.1 or later, you can skip this part since it will be already installed in your solution.
For the solution was created before v3.1, follow the steps below to configure your angular project:
- Add
@abp/ng.schematicspackage to thedevDependenciesof the Angular project (run the following command in the root folder of the angular application):
npm install @abp/ng.schematics --save-dev
- Add
rootNamespaceentry into the/apis/default/section in the/src/environments/environment.ts, as shown below:
apis: {
default: {
...
rootNamespace: 'Acme.BookStore' //<-- ADD THIS
},
}
Acme.BookStore should be replaced by the root namespace of your .NET project. This ensures to not create unnecessary nested folders while creating the service proxy code.
Basic Usage
Project Creation
Assuming you've created your project with the Angular UI.
Example (using the ABP CLI):
abp new AngularProxyDemo -u angular
Run the Application
The backend application must be up and running to be able to use the service proxy code generation system.
See the getting started guide if you don't know how create and to run the project.
Backend
Assume that we have an IBookAppService interface:
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Books
{
public interface IBookAppService : IApplicationService
{
public Task<List<BookDto>> GetListAsync();
}
}
That uses a BookDto defined as shown:
using System;
using Volo.Abp.Application.Dtos;
namespace AngularProxyDemo.Books
{
public class BookDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime PublishDate { get; set; }
}
}
And implemented as the following:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Books
{
public class BookAppService : ApplicationService, IBookAppService
{
public async Task<List<BookDto>> GetListAsync()
{
//TODO: get books from a database...
}
}
}
It simply returns a list of books. You probably want to get the books from a database, but it doesn't matter for this article.
HTTP API
Thanks to the conventional API controllers system of the ABP Framework, we don't have too develop API controllers manually. Just run the backend (HttpApi.Host) application that shows the Swagger UI by default. You will see the GET API for the books:
Service Proxy Generation
Open a command line in the root folder of the Angular application and execute the following command:
abp generate-proxy
It should produce an output like the following:
CREATE src/app/shared/models/books/index.ts (142 bytes)
CREATE src/app/shared/services/books/book.service.ts (437 bytes)
...
generate-proxycommand can take some some optional parameters for advanced scenarios (like modular development). You can take a look at the documentation.
It basically creates two files;
src/app/shared/services/books/book.service.ts: This is the service that can be injected and used to get the list of books;
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { BookDto } from '../../models/book';
@Injectable({
providedIn: 'root',
})
export class BookService {
apiName = 'Default';
getList = () =>
this.restService.request<any, BookDto[]>({
method: 'GET',
url: `/api/app/book`,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
src/app/shared/models/books/index.ts: This file contains the modal classes corresponding to the DTOs defined in the server side;
import type { EntityDto } from '@abp/ng.core';
export interface BookDto extends EntityDto<string> {
name: string;
publishDate: string;
}
You can now inject the BookService into any Angular component and use the getList() method to get the list of books.
About the Generated Code
The generated code is;
- Simple: It is almost identical to the code if you've written it yourself.
- Splitted: Instead of a single, large file;
- It creates a separate
.tsfile for every backend service. Model (DTO) classes are also grouped per service. - It understands the modularity, so creates the services for your own module (or the module you've specified).
- It creates a separate
- Object oriented;
- Supports inheritance of server side DTOs and generates the code respecting to the inheritance structure.
- Supports generic types.
- Supports re-using type definitions across services and doesn't generate the same DTO multiple times.
- Well-aligned to the backend;
- Service method signatures match exactly with the services on the backend services. This is achieved by a special endpoint exposed by the ABP Framework that well defines the backend contracts.
- Namespaces are exactly matches to the backend services and DTOs.
- Well-aligned with the ABP Framework;
- Recognizes the standard ABP Framework DTO types (like
EntityDto,ListResultDto... etc) and doesn't repeat these classes in the application code, but uses from the@abp/ng.corepackage. - Uses the
RestServicedefined by the@abp/ng.corepackage which simplifies the generated code, keeps it short and re-uses all the logics implemented by theRestService(including error handling, authorization token injection, using multiple server endpoints... etc).
- Recognizes the standard ABP Framework DTO types (like
These are the main motivations behind the decision of creating a service proxy generation system, instead of using a pre-built tool like NSWAG.
Other Examples
Let me show you a few more examples.
Updating an Entity
Assume that you added a new method to the server side application service, to update a book:
public Task<BookDto> UpdateAsync(Guid id, BookUpdateDto input);
BookUpdateDto is a simple class defined shown below:
using System;
namespace AngularProxyDemo.Books
{
public class BookUpdateDto
{
public string Name { get; set; }
public DateTime PublishDate { get; set; }
}
}
Let's re-run the generate-proxy command to see the result:
abp generate-proxy
The output of this command will be like the following:
UPDATE src/app/shared/services/books/book.service.ts (660 bytes)
UPDATE src/app/shared/models/books/index.ts (217 bytes)
It tells us two files have been updated. Let's see the changes;
book.service.ts
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { BookDto, BookUpdateDto } from '../../models/books';
@Injectable({
providedIn: 'root',
})
export class BookService {
apiName = 'Default';
getList = () =>
this.restService.request<any, BookDto[]>({
method: 'GET',
url: `/api/app/book`,
},
{ apiName: this.apiName });
update = (id: string, input: BookUpdateDto) =>
this.restService.request<any, BookDto>({
method: 'PUT',
url: `/api/app/book/${id}`,
body: input,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
update function has been added to the BookService that gets an id and a BookUpdateDto as the parameters.
index.ts
import type { EntityDto } from '@abp/ng.core';
export interface BookDto extends EntityDto<string> {
name: string;
publishDate: string;
}
export interface BookUpdateDto {
name: string;
publishDate: string;
}
Added a new DTO class: BookUpdateDto.
Advanced Example
In this example, I want to show a DTO structure using inheritance, generics, arrays and dictionaries.
I've created an IOrderAppService as shown below:
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Orders
{
public interface IOrderAppService : IApplicationService
{
public Task CreateAsync(OrderCreateDto input);
}
}
OrderCreateDto and the related DTOs are as the followings;
using System;
using System.Collections.Generic;
using Volo.Abp.Data;
namespace AngularProxyDemo.Orders
{
public class OrderCreateDto : IHasExtraProperties
{
public Guid CustomerId { get; set; }
public DateTime CreationTime { get; set; }
//ARRAY of DTOs
public OrderDetailDto[] Details { get; set; }
//DICTIONARY
public Dictionary<string, object> ExtraProperties { get; set; }
}
public class OrderDetailDto : GenericDetailDto<int> //INHERIT from GENERIC
{
public string Note { get; set; }
}
//GENERIC class
public abstract class GenericDetailDto<TCount>
{
public Guid ProductId { get; set; }
public TCount Count { get; set; }
}
}
When I run the abp generate-proxy command again, I see two new files have been created.
src/app/shared/services/orders/order.service.ts
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { OrderCreateDto } from '../../models/orders';
@Injectable({
providedIn: 'root',
})
export class OrderService {
apiName = 'Default';
create = (input: OrderCreateDto) =>
this.restService.request<any, void>({
method: 'POST',
url: `/api/app/order`,
body: input,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
src/app/shared/models/orders/index.ts
import type { GenericDetailDto } from '../../models/orders';
export interface OrderCreateDto {
customerId: string;
creationTime: string;
details: OrderDetailDto[];
extraProperties: string | object;
}
export interface OrderDetailDto extends GenericDetailDto<number> {
note: string;
}
NOTE: 3.1.0-rc2 was generating the code above, which is wrong. It will be fixed in the next RC versions.
