mirror of https://github.com/abpframework/abp.git
249 changed files with 4374 additions and 1975 deletions
@ -0,0 +1,217 @@ |
|||
# Static C# API Client Proxies |
|||
|
|||
ABP can create C# API client proxy code to call your remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level details to call remote services and get results. |
|||
|
|||
Static C# proxies automatically handle the following stuff for you; |
|||
|
|||
* Maps C# **method calls** to remote server **HTTP calls** by considering the HTTP method, route, query string parameters, request payload and other details. |
|||
* **Authenticates** the HTTP Client by adding access token to the HTTP header. |
|||
* **Serializes** to and deserialize from JSON. |
|||
* Handles HTTP API **versioning**. |
|||
* Add **correlation id**, current **tenant** id and the current **culture** to the request. |
|||
* Properly **handles the error messages** sent by the server and throws proper exceptions. |
|||
|
|||
This system can be used by any type of .NET client to consume your HTTP APIs. |
|||
|
|||
## Static vs Dynamic Client Proxies |
|||
|
|||
ABP provides **two types** of client proxy generation system. This document explains the **static client proxies**, which generates client-side code in your development time. You can also see the [Dynamic C# API Client Proxies](Dynamic-CSharp-API-Clients.md) documentation to learn how to use proxies generated on runtime. |
|||
|
|||
Development-time (static) client proxy generation has a **performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**. |
|||
|
|||
## 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, typically in the `Application.Contracts` project if you've created your solution using the startup templates. |
|||
|
|||
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 configure the application for the static client proxy generation. Example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule), //used to create client proxies |
|||
typeof(BookStoreApplicationContractsModule) //contains the application service interfaces |
|||
)] |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
// Prepare for static client proxy generation |
|||
context.Services.AddStaticHttpClientProxies( |
|||
typeof(BookStoreApplicationContractsModule).Assembly |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddStaticHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, and prepares for static client proxy generation. |
|||
|
|||
> The [application startup template](../Startup-Templates/Application.md) comes pre-configured for the **dynamic** client proxy generation, in the `HttpApi.Client` project. If you want to switch to the **static** client proxies, change `context.Services.AddHttpClientProxies` to `context.Services.AddStaticHttpClientProxies` in the module class of your `HttpApi.Client` project. |
|||
|
|||
### Endpoint Configuration |
|||
|
|||
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. The simplest configuration is shown below: |
|||
|
|||
```json |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
See the *AbpRemoteServiceOptions* section below for more detailed configuration. |
|||
|
|||
### Code Generation |
|||
|
|||
Server side must be up and running while generating the client proxy code. So, run your application that serves the HTTP APIs on the `BaseUrl` that is configured like explained in the *Endpoint Configuration* section. |
|||
|
|||
Open a command-line terminal in the root folder of your client project (`.csproj`) and type the following command: |
|||
|
|||
````bash |
|||
abp generate-proxy -t csharp -u http://localhost:53929/ |
|||
```` |
|||
|
|||
> If you haven't installed yet, you should install the [ABP CLI](../CLI.md). |
|||
|
|||
This command should generate the following files under the `ClientProxies` folder: |
|||
|
|||
 |
|||
|
|||
`BookClientProxy.Generated.cs` is the actual generated proxy class in this example. `BookClientProxy` is a `partial` class where you can write your custom code (ABP won't override it). `app-generate-proxy.json` contains information about the remote HTTP endpoint, so ABP can properly perform HTTP requests. |
|||
|
|||
> `generate-proxy` command generates proxies for only the APIs you've defined in your application. If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../CLI.md) documentation for other options. |
|||
|
|||
## Usage |
|||
|
|||
It's straightforward to use the client proxies. 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 DoItAsync() |
|||
{ |
|||
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 static client proxy implementation makes an HTTP call whenever a service method is called by the client. |
|||
|
|||
## Configuration |
|||
|
|||
### AbpRemoteServiceOptions |
|||
|
|||
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can configure it in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) 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/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddStaticHttpClientProxies` method can get an additional parameter for the remote service name. Example: |
|||
|
|||
````csharp |
|||
context.Services.AddStaticHttpClientProxies( |
|||
typeof(BookStoreApplicationContractsModule).Assembly, |
|||
remoteServiceConfigurationName: "BookStore" |
|||
); |
|||
```` |
|||
|
|||
`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. |
|||
|
|||
### Retry/Failure Logic & Polly Integration |
|||
|
|||
If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class. |
|||
|
|||
**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to re-try 3 times on a failure** |
|||
|
|||
````csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<AbpHttpClientBuilderOptions>(options => |
|||
{ |
|||
options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => |
|||
{ |
|||
clientBuilder.AddTransientHttpErrorPolicy(policyBuilder => |
|||
policyBuilder.WaitAndRetryAsync( |
|||
3, |
|||
i => TimeSpan.FromSeconds(Math.Pow(2, i)) |
|||
) |
|||
); |
|||
}); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method. |
|||
|
|||
## See Also |
|||
|
|||
* [Dynamic C# Client Proxies](Dynamic-CSharp-API-Clients.md) |
|||
@ -0,0 +1,51 @@ |
|||
# ABP.IO Platform 5.2 Final Has Been Released! |
|||
|
|||
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 5.2 versions have been released today. |
|||
|
|||
## What's New With 5.2? |
|||
|
|||
Since all the new features are already explained in details with the [5.2 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published) for all the features and enhancements. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 5.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more. |
|||
|
|||
## How to Upgrade an Existing Solution |
|||
|
|||
### Install/Update the ABP CLI |
|||
|
|||
First of all, install the ABP CLI or upgrade to the latest version. |
|||
|
|||
If you haven't installed yet: |
|||
|
|||
```bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
To update an existing installation: |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
### ABP UPDATE Command |
|||
|
|||
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
|||
|
|||
```bash |
|||
abp update |
|||
``` |
|||
|
|||
Run this command in the root folder of your solution. |
|||
|
|||
## Migration Guide |
|||
|
|||
Check [the migration guide](https://docs.abp.io/en/abp/5.2/Migration-Guides/Abp-5_2) for the applications with the version 5.x upgrading to the version 5.2. |
|||
|
|||
## About the Next Version |
|||
|
|||
The next feature version will be 5.3. It is planned to release the 5.3 RC (Release Candidate) on May 03 and the final version on May 31, 2022. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). |
|||
|
|||
Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problem with this version. |
|||
@ -0,0 +1,548 @@ |
|||
# Handle Concurrency with EF Core in an ABP Framework Project with ASP.NET Core MVC |
|||
|
|||
In this article, we'll create a basic application to demonstrate how "Concurrency Check/Control" can be implemented in an ABP project. |
|||
|
|||
## Creating the Solution |
|||
|
|||
For this article, we will create a simple BookStore application and add CRUD functionality to the pages. Hence we deal with the concurrency situation. |
|||
|
|||
We can create a new startup template with EF Core as a database provider and MVC for the UI Framework. |
|||
|
|||
> If you already have a project, you don't need to create a new startup template, you can directly implement the following steps to your project. So you can skip this section. |
|||
|
|||
We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI). |
|||
|
|||
```bash |
|||
abp new Acme.BookStore |
|||
``` |
|||
|
|||
After running the above command, our project boilerplate will be downloaded. Then we can open the solution and start the development. |
|||
|
|||
## Starting the Development |
|||
|
|||
Let's start with defining our entities. |
|||
|
|||
### Creating Entities |
|||
|
|||
Create a `Book.cs` (/Books/Book.cs) class in the `.Domain` layer: |
|||
|
|||
```csharp |
|||
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; } |
|||
} |
|||
``` |
|||
|
|||
* To enable **Concurrency Check** for our entities, our entities should be implemented the `IHasConcurrencyStamp` interface, directly or indirectly. |
|||
|
|||
* [Aggregate Root](https://docs.abp.io/en/abp/5.2/Entities#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface, so if we inherit our entities from one of these entity classes then we won't need to manually implement the `IHasConcurrencyStamp` interface. |
|||
|
|||
* And we've derived the `Book` entity from `AuditedAggregateRoot<TKey>` here, so we don't need to implement the `IHasConcurrencyStamp` interface because `AuditedAggregateRoot` class already implemented the `IHasConcurrencyStamp` interface. |
|||
|
|||
> You can read more details from the [Concurrency Check](https://docs.abp.io/en/abp/5.2/Concurrency-Check) documentation. |
|||
|
|||
Then, create a `BookType` (/Books/BookType.cs) enum in the `.Domain.Shared` layer: |
|||
|
|||
```csharp |
|||
public enum BookType |
|||
{ |
|||
Undefined, |
|||
Adventure, |
|||
Biography, |
|||
Dystopia, |
|||
Fantastic, |
|||
Horror, |
|||
Science, |
|||
ScienceFiction, |
|||
Poetry |
|||
} |
|||
``` |
|||
|
|||
### Database Integration |
|||
|
|||
Open the `BookStoreDbContext` (/EntityFrameworkCore/BookStoreDbContext.cs) class in the `*.EntityFrameworkCore` project and add the following `DbSet<Book>` statement: |
|||
|
|||
```csharp |
|||
namespace Acme.BookStore.EntityFrameworkCore; |
|||
|
|||
[ReplaceDbContext(typeof(IIdentityDbContext))] |
|||
[ReplaceDbContext(typeof(ITenantManagementDbContext))] |
|||
[ConnectionStringName("Default")] |
|||
public class BookStoreDbContext : |
|||
AbpDbContext<BookStoreDbContext>, |
|||
IIdentityDbContext, |
|||
ITenantManagementDbContext |
|||
{ |
|||
//Entities from the modules |
|||
|
|||
public DbSet<Book> Books { get; set; } //add this line |
|||
} |
|||
``` |
|||
|
|||
Then we can navigate to the `OnModelCreating` method in the same class and configure our tables/entities: |
|||
|
|||
```csharp |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
|
|||
builder.ConfigurePermissionManagement(); |
|||
... |
|||
|
|||
//* Configure your own tables/entities inside here */ |
|||
|
|||
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); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
After the mapping configurations, we can create a new migration and apply changes to the database. |
|||
|
|||
To do this, open your command line terminal in the directory of the `EntityFrameworkCore` project and run the below command: |
|||
|
|||
```bash |
|||
dotnet ef migrations add Added_Books |
|||
``` |
|||
|
|||
After this command, a new migration will be generated and then we can run the `*.DbMigrator` project to apply the last changes to the database such as creating a new table named `Books` according to the last created migration. |
|||
|
|||
### Defining DTOs and Application Service Interfaces |
|||
|
|||
We can start to define the use cases of the application. |
|||
|
|||
Create the DTO classes (under the **Books** folder) in the `Application.Contracts` project: |
|||
|
|||
**BookDto.cs** |
|||
|
|||
```csharp |
|||
public class BookDto : AuditedEntityDto<Guid>, IHasConcurrencyStamp |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public BookType Type { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
|
|||
public float Price { get; set; } |
|||
|
|||
public string ConcurrencyStamp { get; set; } |
|||
} |
|||
``` |
|||
|
|||
* The `AuditedEntityDto<TKey>` class is not implemented from the `IHasConcurrencyStamp` interface, so for the **BookDto** class we need to implement the `IHasConcurrencyStamp`. |
|||
|
|||
* This is important, because we need to return books with their **ConcurrencyStamp** value. |
|||
|
|||
**CreateBookDto.cs** |
|||
|
|||
```csharp |
|||
public class CreateBookDto |
|||
{ |
|||
[Required] |
|||
[StringLength(128)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public BookType Type { get; set; } = BookType.Undefined; |
|||
|
|||
[Required] |
|||
[DataType(DataType.Date)] |
|||
public DateTime PublishDate { get; set; } = DateTime.Now; |
|||
|
|||
[Required] |
|||
public float Price { get; set; } |
|||
} |
|||
``` |
|||
|
|||
**UpdateBookDto.cs** |
|||
|
|||
```csharp |
|||
public class UpdateBookDto : IHasConcurrencyStamp |
|||
{ |
|||
[Required] |
|||
[StringLength(128)] |
|||
public string Name { get; set; } |
|||
|
|||
[Required] |
|||
public BookType Type { get; set; } = BookType.Undefined; |
|||
|
|||
[Required] |
|||
[DataType(DataType.Date)] |
|||
public DateTime PublishDate { get; set; } = DateTime.Now; |
|||
|
|||
[Required] |
|||
public float Price { get; set; } |
|||
|
|||
public string ConcurrencyStamp { get; set; } |
|||
} |
|||
``` |
|||
|
|||
* Here, we've implemented the `IHasConcurrencyStamp` interface for the **UpdateBookDto** class. |
|||
|
|||
* We will use this value while updating an existing book. ABP Framework will compare the current book's **ConcurrencyStamp** value with the provided one, if values are matched, this means everything is as it is supposed to be and will update the record. |
|||
|
|||
* If values are mismatched, then it means the record that we're trying to update is already updated by another user and we need to get the latest changes to be able to make changes on it. |
|||
|
|||
* Also, in that case, `AbpDbConcurrencyException` will be thrown by the ABP Framework and we can either handle this exception manually or let the ABP Framework handle it on behalf of us and show a user-friendly error message as in the image below. |
|||
|
|||
 |
|||
|
|||
Create a new `IBookAppService` (/Books/IBookAppService.cs) interface in the `Application.Contracts` project: |
|||
|
|||
```csharp |
|||
public interface IBookAppService : |
|||
ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto> |
|||
{ |
|||
} |
|||
``` |
|||
* We've implemented the `ICrudAppService` here, because we just need to perform CRUD operations and this interface helps us define common CRUD operation methods. |
|||
|
|||
### Application Service Implementations |
|||
|
|||
Create a `BookAppService` (/Books/BookAppService.cs) class inside the `*.Application` project and implement the application service methods, as shown below: |
|||
|
|||
```csharp |
|||
public class BookAppService : |
|||
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto>, |
|||
IBookAppService |
|||
{ |
|||
public BookAppService(IRepository<Book, Guid> repository) |
|||
: base(repository) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input) |
|||
{ |
|||
var book = await Repository.GetAsync(id); |
|||
|
|||
book.Name = input.Name; |
|||
book.Price = input.Price; |
|||
book.Type = input.Type; |
|||
book.PublishDate = input.PublishDate; |
|||
|
|||
//set Concurrency Stamp value to the entity |
|||
book.ConcurrencyStamp = input.ConcurrencyStamp; |
|||
|
|||
var updatedBook = await Repository.UpdateAsync(book); |
|||
return ObjectMapper.Map<Book, BookDto>(updatedBook); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* We've used the `CrudAppService` base class. This class implements all common CRUD operations and if we want to change a method, we can simply override the method and change it to our needs. |
|||
|
|||
> Normally, you don't need to override the `UpdateAsync` method to do **Concurrency Check**. Because the `UpdateAsync` method of the `CrudAppService` class by default map input values to the entity. But I wanted to override this method to show what we need to do for **Concurrency Check**. |
|||
|
|||
* We can look closer to the `UpdateAsync` method here, because as we've mentioned earlier we need to pass the provided **ConcurrencyStamp** value to be able to do **Concurrency Check/Control** to our entity while updating. |
|||
|
|||
* At that point, if the given record is already updated by any other user, a **ConcurrencyStamp** mismatch will occur and `AbpDbConcurrencyException` will be thrown thanks to the **Concurrency Check** system of ABP, data-consistency will be provided and the current record won't be overridden. |
|||
|
|||
* And if the values are matched, the record will be updated successfully. |
|||
|
|||
After implementing the application service methods, we can do the related mapping configurations, so open the `BookStoreApplicationAutoMapperProfile.cs` and update the content as below: |
|||
|
|||
```csharp |
|||
public class BookStoreApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<Book, BookDto>(); |
|||
CreateMap<CreateBookDto, Book>(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### User Interface |
|||
|
|||
So far, we've applied the all necessary steps for the **Concurrency Check** system, let's see it in action. |
|||
|
|||
Create a razor page in the `.Web` layer named `Index` (**/Pages/Books/Index.cshtml**), open this file and replace the content with the following code block: |
|||
|
|||
```html |
|||
@page |
|||
@using Acme.BookStore.Localization |
|||
@using Microsoft.Extensions.Localization |
|||
@model Acme.BookStore.Web.Pages.Books.Index |
|||
|
|||
@section scripts |
|||
{ |
|||
<abp-script src="/Pages/Books/Index.js" /> |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<abp-card-title>Books</abp-card-title> |
|||
</abp-column> |
|||
<abp-column size-md="_6" class="text-end"> |
|||
<abp-button id="NewBookButton" |
|||
text="New Book" |
|||
icon="plus" |
|||
button-type="Primary"/> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-table striped-rows="true" id="BooksTable"></abp-table> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
|
|||
* We've defined a table and "New Book" button inside a card element here, we'll fill the table with our book records in the next step by using the **Datatables** library. |
|||
|
|||
Create an `Index.js` (**/Pages/Books/Index.js**) file and add the following code block: |
|||
|
|||
```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({ |
|||
serverSide: true, |
|||
paging: true, |
|||
order: [[1, "asc"]], |
|||
searching: false, |
|||
scrollX: true, |
|||
ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), |
|||
columnDefs: [ |
|||
{ |
|||
title: l('Actions'), |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
title: l('Name'), |
|||
data: "name" |
|||
}, |
|||
{ |
|||
title: l('Type'), |
|||
data: "type", |
|||
render: function (data) { |
|||
return l('Enum:BookType:' + data); |
|||
} |
|||
}, |
|||
{ |
|||
title: l('PublishDate'), |
|||
data: "publishDate", |
|||
render: function (data) { |
|||
return luxon |
|||
.DateTime |
|||
.fromISO(data, { |
|||
locale: abp.localization.currentCulture.name |
|||
}).toLocaleString(); |
|||
} |
|||
}, |
|||
{ |
|||
title: l('Price'), |
|||
data: "price" |
|||
}, |
|||
{ |
|||
title: l('CreationTime'), |
|||
data: "creationTime", |
|||
render: function (data) { |
|||
return luxon |
|||
.DateTime |
|||
.fromISO(data, { |
|||
locale: abp.localization.currentCulture.name |
|||
}).toLocaleString(luxon.DateTime.DATETIME_SHORT); |
|||
} |
|||
} |
|||
] |
|||
}) |
|||
); |
|||
|
|||
createModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
editModal.onResult(function () { |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
|
|||
$('#NewBookButton').click(function (e) { |
|||
e.preventDefault(); |
|||
createModal.open(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
* We've used the [Datatables](https://datatables.net/) to list our books. |
|||
|
|||
* Also defined **create** and **update** modals by using [ABP Modal Manager](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Modals#modalmanager-reference), but we didn't create them yet, so let's create the modals. |
|||
|
|||
First, create a **CreateModal** razor page and update the **CreateModal.cshtml** and **CreateModal.cshtml.cs** files as below: |
|||
|
|||
**CreateModal.cshtml** |
|||
|
|||
```html |
|||
@page |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model CreateModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="New Book"></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> |
|||
``` |
|||
|
|||
* We've used `abp-dynamic-form` tag-helper and passed it a `Book` model, this tag helper will simply create form contents (inputs, select boxes etc.) on behalf of us. |
|||
|
|||
* **CreateModal.cshtml.cs** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Acme.BookStore.Books; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Acme.BookStore.Web.Pages.Books; |
|||
|
|||
public class CreateModalModel : BookStorePageModel |
|||
{ |
|||
[BindProperty] |
|||
public CreateBookDto Book { get; set; } |
|||
|
|||
private readonly IBookAppService _bookAppService; |
|||
|
|||
public CreateModalModel(IBookAppService bookAppService) |
|||
{ |
|||
_bookAppService = bookAppService; |
|||
} |
|||
|
|||
public void OnGet() |
|||
{ |
|||
Book = new CreateBookDto(); |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.CreateAsync(Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* In this file, we simply define **CreateBookDto** as a bind property and we'll use this class's properties in the form. Thanks to the `abp-dynamic-form` tag-helper we don't need to define all of these form elements one by one, it will generate on behalf of us. |
|||
|
|||
We can create an **EditModal** razor page and update the **EditModal.cshtml** and **EditModal.cshtml.cs** files as below: |
|||
|
|||
**EditModal.cshtml** |
|||
|
|||
```html |
|||
@page |
|||
@using Acme.BookStore.Web.Pages.Books |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal |
|||
@model EditModalModel |
|||
@{ |
|||
Layout = null; |
|||
} |
|||
<form asp-page="/Books/EditModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="Update"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-input asp-for="Id"/> |
|||
<abp-input asp-for="Book.Name"/> |
|||
<abp-input asp-for="Book.Price"/> |
|||
<abp-select asp-for="Book.Type"/> |
|||
<abp-input asp-for="Book.PublishDate"/> |
|||
<abp-input asp-for="Book.ConcurrencyStamp" type="hidden"/> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</form> |
|||
``` |
|||
|
|||
* Here, we didn't use the `abp-dynamic-form` tag-helper and added all the necessary form elements to our form one by one. |
|||
|
|||
* As you may have noticed, we've set the input type as **hidden** for the **ConcurrencyStamp** input, because the end-user should not see this value. |
|||
|
|||
> Instead of doing it like that, we could create a view model class and use the `[HiddenInput]` data attribute for the **ConcurrencyStamp** property and use the `abp-dynamic-form` tag-helper. But to simplify the article I didn't want to do that, if you want you can create a view model and define the necessary data attributes for properties. |
|||
|
|||
**EditModal.cshtml.cs** |
|||
|
|||
```csharp |
|||
public class EditModalModel : BookStorePageModel |
|||
{ |
|||
[HiddenInput] |
|||
[BindProperty(SupportsGet = true)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[BindProperty] |
|||
public UpdateBookDto 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, UpdateBookDto>(bookDto); |
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
await _bookAppService.UpdateAsync(Id, Book); |
|||
return NoContent(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Lastly, we can define the necessary mapping configurations and run the application to see the result. |
|||
|
|||
Open the `BookStoreWebAutoMapperProfile.cs` class and update the content as below: |
|||
|
|||
```csharp |
|||
public class BookStoreWebAutoMapperProfile : Profile |
|||
{ |
|||
public BookStoreWebAutoMapperProfile() |
|||
{ |
|||
CreateMap<BookDto, UpdateBookDto>(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Then we can run the application, navigate to the **/Books** endpoint and see the result. |
|||
|
|||
 |
|||
|
|||
* In the image above, we can see that multiple users open the edit model to change a record and try to update the relevant record independently of each other. |
|||
|
|||
* After the first user updated the record, the second user tries to update the same record without getting the last state of the record. And therefore `AbpDbConcurrencyException` is thrown because **ConcurrencyStamp** values are different from each other. |
|||
|
|||
* The second user should close and re-open the model to get the last state of the record and then they can make changes to the current record. |
|||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,113 @@ |
|||
# Dealing with Multiple Implementations of a Service in ASP.NET Core & ABP Dependency Injection |
|||
|
|||
ASP.NET Core provides a built-in [dependency injection system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) to register your services to the dependency injection container and inject/resolve them whenever you need. ABP's [dependency injection infrastructure](https://docs.abp.io/en/abp/latest/Dependency-Injection) is built on ASP.NET Core's DI system, automates service registration by conventions and provides some additional features. |
|||
|
|||
In this tutorial, I will explain how you can register multiple implementations of the same service interface and inject/resolve all these implementations when you need them. |
|||
|
|||
## Defining Services |
|||
|
|||
Assume that you have an `IExternalLogger` interface with two implementations: |
|||
|
|||
````csharp |
|||
public interface IExternalLogger |
|||
{ |
|||
Task LogAsync(string logText); |
|||
} |
|||
|
|||
public class ElasticsearchExternalLogger : IExternalLogger |
|||
{ |
|||
public async Task LogAsync(string logText) |
|||
{ |
|||
// TODO... |
|||
} |
|||
} |
|||
|
|||
public class AzureExternalLogger : IExternalLogger |
|||
{ |
|||
public async Task LogAsync(string logText) |
|||
{ |
|||
// TODO... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
An example service injecting the `IExternalLogger` interface: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IExternalLogger _externalLogger; |
|||
|
|||
public MyService(IExternalLogger externalLogger) |
|||
{ |
|||
_externalLogger = externalLogger; |
|||
} |
|||
|
|||
public async Task DemoAsync() |
|||
{ |
|||
await _externalLogger.LogAsync("Example log message..."); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In this example, we haven't registered any of the implementation classes to the dependency injection system yet. So, if we try to use the `MyService` class, we get an error indicating that no implementation found for the `IExternalLogger` service. We should register at least one implementation for the `IExternalLogger` interface. |
|||
|
|||
## Registering Services |
|||
|
|||
If we register both of the `ElasticsearchExternalLogger` and `AzureExternalLogger` services for the `IExternalLogger` interface, and then try to inject the `IExternalLogger` interface, the last registered implementation will be used. However, how to determine the last registered implementation? |
|||
|
|||
If we implement one of the [dependency interfaces](https://docs.abp.io/en/abp/latest/Dependency-Injection#dependency-interfaces) (e.g. `ITransientDependency`), then the registration order will be uncertain (it may depend on the namespaces of the classes). The *last registered implementation* can be different than you expect. So, it is not suggested to use the dependency interfaces to register multiple implementations. |
|||
|
|||
You can register your services in the `ConfigureServices` method of your module: |
|||
|
|||
````csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddTransient<IExternalLogger, ElasticsearchExternalLogger>(); |
|||
context.Services.AddTransient<IExternalLogger, AzureExternalLogger>(); |
|||
} |
|||
```` |
|||
|
|||
In this case, you get an `AzureExternalLogger` instance when you inject the `IExternalLogger` interface, because the last registered implementation is the `AzureExternalLogger` class. |
|||
|
|||
## Injecting Multiple Implementations |
|||
|
|||
When you have multiple implementation of an interface, you may want to work with all these implementations. For this example, you may want to write log to all the external loggers. We can change the `MyService` implementation as the following: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IEnumerable<IExternalLogger> _externalLoggers; |
|||
|
|||
public MyService(IEnumerable<IExternalLogger> externalLoggers) |
|||
{ |
|||
_externalLoggers = externalLoggers; |
|||
} |
|||
|
|||
public async Task DemoAsync() |
|||
{ |
|||
foreach (var externalLogger in _externalLoggers) |
|||
{ |
|||
await externalLogger.LogAsync("Example log message..."); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In this example, we are injecting `IEnumerable<IExternalLogger>` instead of `IExternalLogger`, so we have a collection of the `IExternalLogger` implementations. Then we are using a `foreach` loop to write the same log text to all the `IExternalLogger` implementations. |
|||
|
|||
If you are using `IServiceProvider` to resolve dependencies, then use its `GetServices` method to obtain a collection of the service implementations: |
|||
|
|||
````csharp |
|||
IEnumerable<IExternalLogger> services = _serviceProvider.GetServices<IExternalLogger>(); |
|||
```` |
|||
|
|||
## Further Reading |
|||
|
|||
In this small tutorial, I explained how you can register multiple implementations of the same interface to the dependency injection system and inject/resolve all of them when you need. |
|||
|
|||
If you want to get more information about ABP's and ASP.NET Core's dependency injection systems, you can read the following documents: |
|||
|
|||
* [ABP's Dependency Injection documentation](https://docs.abp.io/en/abp/latest/Dependency-Injection) |
|||
* [ASP.NET Core Dependency Injection best practices, tips & tricks](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) |
|||
* [ASP.NET Core's Dependency Injection documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) |
|||
@ -1,8 +1,3 @@ |
|||
# Getting Started with the Startup Templates |
|||
This document has been [moved to here](Getting-Started.md). |
|||
|
|||
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: |
|||
|
|||
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No) |
|||
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No) |
|||
|
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
<!-- TODO: this document has been moved, it should be deleted in the future. --> |
|||
|
|||
@ -0,0 +1,157 @@ |
|||
# Static JavaScript API Client Proxies |
|||
|
|||
It is typical to consume your HTTP APIs from your JavaScript code. To do that, you normally deal with low level AJAX calls, like $.ajax, or better [abp.ajax](JavaScript-API/Ajax.md). ABP Framework provides **a better way** to call your HTTP APIs from your JavaScript code: JavaScript API Client Proxies! |
|||
|
|||
## Static vs Dynamic JavaScript Client Proxies |
|||
|
|||
ABP provides **two types** of client proxy generation system. This document explains the **static client proxies**, which generates client-side code in your development time. You can also see the [Dynamic JavaScript API Client Proxies](Dynamic-JavaScript-Proxies.md) documentation to learn how to use proxies generated on runtime. |
|||
|
|||
Development-time (static) client proxy generation has a **slight performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**. |
|||
|
|||
## A Quick Example |
|||
|
|||
### The Application Service |
|||
|
|||
Assume that you have an application service defined as shown below: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace Acme.BookStore.Authors |
|||
{ |
|||
public interface IAuthorAppService : IApplicationService |
|||
{ |
|||
Task<AuthorDto> GetAsync(Guid id); |
|||
|
|||
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input); |
|||
|
|||
Task<AuthorDto> CreateAsync(CreateAuthorDto input); |
|||
|
|||
Task UpdateAsync(Guid id, UpdateAuthorDto input); |
|||
|
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> You can follow the [web application development tutorial](../../Tutorials/Part-1.md) to learn how to create [application services](../../Application-Services.md), expose them as [HTTP APIs](../../API/Auto-API-Controllers.md) and consume from the JavaScript code as a complete example. |
|||
|
|||
### Generating the JavaScript Code |
|||
|
|||
Server side must be up and running while generating the client proxy code. So, first run the application that hosts your HTTP APIs (can be the Web application or the HttpApi.Host application depending on your solution structure). |
|||
|
|||
Open a command-line terminal in the root folder of your web project (`.csproj`) and type the following command: |
|||
|
|||
````bash |
|||
abp generate-proxy -t js -u https://localhost:53929/ |
|||
```` |
|||
|
|||
> If you haven't installed yet, you should install the [ABP CLI](../CLI.md). Change the example URL to your application's root URL. |
|||
|
|||
This command should generate the following files under the `ClientProxies` folder: |
|||
|
|||
 |
|||
|
|||
`app-proxy.js` is the generated proxy file in this example. Here, an example proxy function in this file: |
|||
|
|||
````js |
|||
acme.bookStore.authors.author.get = function(id, ajaxParams) { |
|||
return abp.ajax($.extend(true, { |
|||
url: abp.appPath + 'api/app/author/' + id + '', |
|||
type: 'GET' |
|||
}, ajaxParams)); |
|||
}; |
|||
```` |
|||
|
|||
> `generate-proxy` command generates proxies for only the APIs you've defined in your application (assumes `app` as the module name). If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../CLI.md) documentation for other options. |
|||
|
|||
### Using the Proxy Functions |
|||
|
|||
To use the proxy functions, first import the `app-proxy.js` file into your page: |
|||
|
|||
````html |
|||
<abp-script src="/client-proxies/app-proxy.js"/> |
|||
```` |
|||
|
|||
> We've used the [abp-script tag helper](Bundling-Minification.md) in this example. You could use the standard `script` tag, but the `abp-script` is the recommended way of importing JavaScript files to your pages. |
|||
|
|||
Now, you can call any of the application service methods from your JavaScript code, just like calling a JavaScript function. The JavaScript function has the identical function **name**, **parameters** and the **return value** with the C# method. |
|||
|
|||
**Example: Get a single author** |
|||
|
|||
````js |
|||
acme.bookStore.authors.author |
|||
.get("7245a066-5457-4941-8aa7-3004778775f0") //Get id from somewhere! |
|||
.then(function(result){ |
|||
console.log(result); |
|||
}); |
|||
```` |
|||
|
|||
**Example: Get the authors list** |
|||
|
|||
````js |
|||
acme.bookStore.authors.author.getList({ |
|||
maxResultCount: 10 |
|||
}).then(function(result){ |
|||
console.log(result.items); |
|||
}); |
|||
```` |
|||
|
|||
**Example: Delete an author** |
|||
|
|||
```js |
|||
acme.bookStore.authors.author |
|||
.delete('7245a066-5457-4941-8aa7-3004778775f0') //Get id from somewhere! |
|||
.then(function() { |
|||
abp.notify.info('Successfully deleted!'); |
|||
}); |
|||
``` |
|||
|
|||
## Disabling Dynamic JavaScript Proxies |
|||
|
|||
When you create an application or module, the [dynamic client proxy generation](Dynamic-JavaScript-Proxies.md) approach is used by default. If you want to use the statically generated client proxies for your application, you should explicitly disable it for your application or module in the `ConfigureServices` method of your [module class](../../Module-Development-Basics.md) as like in the following example: |
|||
|
|||
````csharp |
|||
Configure<DynamicJavaScriptProxyOptions>(options => |
|||
{ |
|||
options.DisableModule("app"); |
|||
}); |
|||
```` |
|||
|
|||
`app` represents the main application in this example, which works if you are creating an application. If you are developing an application module, then use your module's name. |
|||
|
|||
## AJAX Details |
|||
|
|||
JavaScript client proxy functions use the [abp.ajax](JavaScript-API/Ajax.md) under the hood. So, you have the same benefits like **automatic error handling**. Also, you can fully control the AJAX call by providing the options. |
|||
|
|||
### The Return Value |
|||
|
|||
Every function returns a [Deferred object](https://api.jquery.com/category/deferred-object/). That means you can chain with `then` to get the result, `catch` to handle the error, `always` to perform an action once the operation completes (success or failed). |
|||
|
|||
### AJAX Options |
|||
|
|||
Every function gets an additional **last parameter** after your own parameters. The last parameter is called as `ajaxParams`. It is an object that overrides the AJAX options. |
|||
|
|||
**Example: Set `type` and `dataType` AJAX options** |
|||
|
|||
````js |
|||
acme.bookStore.authors.author |
|||
.delete('7245a066-5457-4941-8aa7-3004778775f0', { |
|||
type: 'POST', |
|||
dataType: 'xml' |
|||
}) |
|||
.then(function() { |
|||
abp.notify.info('Successfully deleted!'); |
|||
}); |
|||
```` |
|||
|
|||
See the [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) documentation for all the available options. |
|||
|
|||
## See Also |
|||
|
|||
* [Dynamic JavaScript API Client Proxies](Dynamic-JavaScript-Proxies.md) |
|||
* [Auto API Controllers](../../API/Auto-API-Controllers.md) |
|||
* [Web Application Development Tutorial](../../Tutorials/Part-1.md) |
|||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,21 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps; |
|||
|
|||
public class RemoveFileStep : ProjectBuildPipelineStep |
|||
{ |
|||
private readonly string _filePath; |
|||
public RemoveFileStep(string filePath) |
|||
{ |
|||
_filePath = filePath; |
|||
} |
|||
|
|||
public override void Execute(ProjectBuildContext context) |
|||
{ |
|||
var fileToRemove = context.Files.Find(x => x.Name.EndsWith(_filePath)); |
|||
if (fileToRemove != null) |
|||
{ |
|||
context.Files.Remove(fileToRemove); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using Volo.Abp.Cli.Utils; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.ProjectModification; |
|||
|
|||
public class AngularPwaSupportAdder : ITransientDependency |
|||
{ |
|||
protected ICmdHelper CmdHelper { get; } |
|||
protected PackageJsonFileFinder PackageJsonFileFinder { get; } |
|||
|
|||
public AngularPwaSupportAdder( |
|||
ICmdHelper cmdHelper, |
|||
PackageJsonFileFinder packageJsonFileFinder) |
|||
{ |
|||
CmdHelper = cmdHelper; |
|||
PackageJsonFileFinder = packageJsonFileFinder; |
|||
} |
|||
|
|||
public virtual void AddPwaSupport(string rootDirectory) |
|||
{ |
|||
var fileList = PackageJsonFileFinder.Find(rootDirectory).Where(x => File.Exists(x.RemovePostFix("package.json") + "angular.json")).ToList(); |
|||
|
|||
fileList.ForEach(AddPwaSupportToProject); |
|||
} |
|||
|
|||
protected virtual void AddPwaSupportToProject(string filePath) |
|||
{ |
|||
var directory = Path.GetDirectoryName(filePath).EnsureEndsWith(Path.DirectorySeparatorChar); |
|||
|
|||
CmdHelper.RunCmd("ng add @angular/pwa --skip-confirmation", workingDirectory: directory); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Microsoft.EntityFrameworkCore.Design; |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.EntityFrameworkCore.Design; |
|||
|
|||
public abstract class AbpDesignTimeDbContextBase<TModule, TContext> : IDesignTimeDbContextFactory<TContext> |
|||
where TModule : AbpModule |
|||
where TContext : DbContext |
|||
{ |
|||
public virtual TContext CreateDbContext(string[] args) |
|||
{ |
|||
return AsyncHelper.RunSync(() => CreateDbContextAsync(args)); |
|||
} |
|||
|
|||
protected virtual async Task<TContext> CreateDbContextAsync(string[] args) |
|||
{ |
|||
var application = await AbpApplicationFactory.CreateAsync<TModule>(options => |
|||
{ |
|||
options.Services.ReplaceConfiguration(BuildConfiguration()); |
|||
ConfigureServices(options.Services); |
|||
}); |
|||
|
|||
await application.InitializeAsync(); |
|||
|
|||
return application.ServiceProvider.GetRequiredService<TContext>(); |
|||
} |
|||
|
|||
protected virtual void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected abstract IConfigurationRoot BuildConfiguration(); |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using Shouldly; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Uow; |
|||
|
|||
public class UnitOfWorkExtensions_Tests : AbpIntegratedTest<AbpUnitOfWorkModule> |
|||
{ |
|||
private readonly IUnitOfWorkManager _unitOfWorkManager; |
|||
|
|||
public UnitOfWorkExtensions_Tests() |
|||
{ |
|||
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AddItem() |
|||
{ |
|||
var uow = _unitOfWorkManager.Begin(); |
|||
uow.AddItem("testKey", "testValue"); |
|||
|
|||
uow.Items.ShouldContainKey("testKey"); |
|||
uow.Items.ContainsValue("testValue"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetItemOrDefault() |
|||
{ |
|||
var uow = _unitOfWorkManager.Begin(); |
|||
uow.Items.Add("testKey", new NameValue("TestKey","TestValue")); |
|||
|
|||
uow.GetItemOrDefault<NameValue>("testKey").ShouldBeOfType<NameValue>(); |
|||
uow.GetItemOrDefault<NameValue>("testKey").Value.ShouldBe("TestValue"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetOrAddItem() |
|||
{ |
|||
var uow = _unitOfWorkManager.Begin(); |
|||
|
|||
var item = uow.GetOrAddItem("testKey", _ => new NameValue("TestKey", "TestValue")); |
|||
|
|||
item.Name.ShouldBe("TestKey"); |
|||
item.ShouldBeOfType<NameValue>(); |
|||
item.Value.ShouldBe("TestValue"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RemoveItem() |
|||
{ |
|||
var uow = _unitOfWorkManager.Begin(); |
|||
uow.Items.Add("testKey", "testValue"); |
|||
|
|||
uow.RemoveItem("testKey"); |
|||
|
|||
uow.Items.ShouldNotContainKey("testKey"); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.GlobalFeatures; |
|||
using Volo.CmsKit.GlobalFeatures; |
|||
|
|||
namespace Volo.CmsKit.Blogs; |
|||
|
|||
public class BlogFeatureDataSeedContributor : IDataSeedContributor, ITransientDependency |
|||
{ |
|||
private readonly BlogFeatureManager _blogFeatureManager; |
|||
private readonly IBlogRepository _blogRepository; |
|||
|
|||
public BlogFeatureDataSeedContributor( |
|||
BlogFeatureManager blogFeatureManager, |
|||
IBlogRepository blogRepository) |
|||
{ |
|||
_blogFeatureManager = blogFeatureManager; |
|||
_blogRepository = blogRepository; |
|||
} |
|||
|
|||
public async Task SeedAsync(DataSeedContext context) |
|||
{ |
|||
if (!GlobalFeatureManager.Instance.IsEnabled<BlogsFeature>()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var blogs = await _blogRepository.GetListAsync(); |
|||
|
|||
foreach (var blog in blogs) |
|||
{ |
|||
await _blogFeatureManager.SetDefaultsIfNotSetAsync(blog.Id); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue