mirror of https://github.com/abpframework/abp.git
300 changed files with 6488 additions and 1877 deletions
@ -0,0 +1,28 @@ |
|||
# Authorization in Angular UI |
|||
|
|||
OAuth is preconfigured in Angular application templates. So, when you start a project using the CLI (or Suite, for that matter), authorization already works. You can find **OAuth configuration** in the _environment.ts_ files. |
|||
|
|||
```js |
|||
import { Config } from '@abp/ng.core'; |
|||
|
|||
const baseUrl = 'http://localhost:4200'; |
|||
|
|||
export const environment = { |
|||
// other options removed for sake of brevity |
|||
|
|||
oAuthConfig: { |
|||
issuer: 'https://localhost:44305', |
|||
redirectUri: baseUrl, |
|||
clientId: 'MyProjectName_App', |
|||
responseType: 'code', |
|||
scope: 'offline_access MyProjectName', |
|||
}, |
|||
|
|||
// other options removed for sake of brevity |
|||
} as Config.Environment; |
|||
|
|||
``` |
|||
|
|||
This configuration results in an [OAuth authorization code flow with PKCE](https://tools.ietf.org/html/rfc7636) and we are using [angular-oauth2-oidc library](https://github.com/manfredsteyer/angular-oauth2-oidc#logging-in) for managing OAuth in the Angular client. |
|||
|
|||
According to this flow, the user is redirected to an external login page which is built with MVC. So, if you need **to customize the login page**, please follow [this community article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). |
|||
@ -0,0 +1,64 @@ |
|||
# Page Alerts |
|||
|
|||
A page alert is useful for displaying an important message to the user. The ABP Framework provides an easy way to show the following alert to the user. |
|||
|
|||
 |
|||
|
|||
You can simply import `PageAlertService` from `@abp/ng.theme.shared` and utilize it as follows: |
|||
|
|||
```typescript |
|||
import { PageAlertService } from '@abp/ng.theme.shared'; |
|||
|
|||
@Component({ |
|||
// ... |
|||
}) |
|||
export class MyComponent { |
|||
constructor(private service: PageAlertService) {} |
|||
|
|||
showWarning() { |
|||
this.service.show({ |
|||
type: 'warning', |
|||
message: |
|||
'We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!', |
|||
title: 'Service Interruption', |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## `SHOW` |
|||
|
|||
The method `show` accepts a single object that is type of `PageAlert` |
|||
|
|||
```typescript |
|||
export interface PageAlert { |
|||
type: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'; |
|||
message: string; |
|||
title?: string; |
|||
dismissible?: boolean; |
|||
messageLocalizationParams?: string[]; |
|||
titleLocalizationParams?: string[]; |
|||
} |
|||
``` |
|||
|
|||
* `type` (Required): Defines what type of alert will be shown |
|||
* `message` (Required): The message who will be shown, also works with localization as well. |
|||
* `title` (Optional): The title of the message. If it is not provided, the title will be hidden. |
|||
* `dismissible` (Optional): Default is `true`. If enabled, a button on the top right corner will be shown to the users so that they can dismiss the message. |
|||
* `messageLocalizationParams` and `titleLocalizationParams` (Optional): If the message and/or the title is a key for localization service and contains some parameters, these fields could be used to pass those parameters. |
|||
|
|||
### An example with Localization |
|||
|
|||
```typescript |
|||
this.service.show({ |
|||
type: 'danger', |
|||
message: 'AbpAccount::PagerInfo{0}{1}{2}', |
|||
messageLocalizationParams: ['10', '20', '30'], |
|||
title: 'AbpAccount::EntityNotFoundErrorMessage', |
|||
titleLocalizationParams: ['Test', 'id123'], |
|||
}); |
|||
``` |
|||
|
|||
 |
|||
|
|||
|
|||
@ -1,3 +1,376 @@ |
|||
# Angular UI: Testing |
|||
# Unit Testing Angular UI |
|||
|
|||
TODO |
|||
ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.io/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**. |
|||
|
|||
## Setup |
|||
|
|||
In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work. |
|||
|
|||
## Basics |
|||
|
|||
An over-simplified spec file looks like this: |
|||
|
|||
```ts |
|||
import { CoreTestingModule } from "@abp/ng.core/testing"; |
|||
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; |
|||
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; |
|||
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; |
|||
import { NgxValidateCoreModule } from "@ngx-validate/core"; |
|||
import { MyComponent } from "./my.component"; |
|||
|
|||
describe("MyComponent", () => { |
|||
let fixture: ComponentFixture<MyComponent>; |
|||
|
|||
beforeEach( |
|||
waitForAsync(() => { |
|||
TestBed.configureTestingModule({ |
|||
declarations: [MyComponent], |
|||
imports: [ |
|||
CoreTestingModule.withConfig(), |
|||
ThemeSharedTestingModule.withConfig(), |
|||
ThemeBasicTestingModule.withConfig(), |
|||
NgxValidateCoreModule, |
|||
], |
|||
providers: [ |
|||
/* mock providers here */ |
|||
], |
|||
}).compileComponents(); |
|||
}) |
|||
); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(MyComponent); |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it("should be initiated", () => { |
|||
expect(fixture.componentInstance).toBeTruthy(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. |
|||
|
|||
## Tips |
|||
|
|||
### Angular Testing Library |
|||
|
|||
Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. |
|||
|
|||
The simple example above can be written with Angular Testing Library as follows: |
|||
|
|||
```ts |
|||
import { CoreTestingModule } from "@abp/ng.core/testing"; |
|||
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; |
|||
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; |
|||
import { ComponentFixture } from "@angular/core/testing"; |
|||
import { NgxValidateCoreModule } from "@ngx-validate/core"; |
|||
import { render } from "@testing-library/angular"; |
|||
import { MyComponent } from "./my.component"; |
|||
|
|||
describe("MyComponent", () => { |
|||
let fixture: ComponentFixture<MyComponent>; |
|||
|
|||
beforeEach(async () => { |
|||
const result = await render(MyComponent, { |
|||
imports: [ |
|||
CoreTestingModule.withConfig(), |
|||
ThemeSharedTestingModule.withConfig(), |
|||
ThemeBasicTestingModule.withConfig(), |
|||
NgxValidateCoreModule, |
|||
], |
|||
providers: [ |
|||
/* mock providers here */ |
|||
], |
|||
}); |
|||
|
|||
fixture = result.fixture; |
|||
}); |
|||
|
|||
it("should be initiated", () => { |
|||
expect(fixture.componentInstance).toBeTruthy(); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
Very similar, as you can see. The real difference kicks in when we use queries and fire events. |
|||
|
|||
```ts |
|||
// other imports |
|||
import { getByLabelText, screen } from "@testing-library/angular"; |
|||
import userEvent from "@testing-library/user-event"; |
|||
|
|||
describe("MyComponent", () => { |
|||
beforeEach(/* removed for sake of brevity */); |
|||
|
|||
it("should display advanced filters", () => { |
|||
const filters = screen.getByTestId("author-filters"); |
|||
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; |
|||
expect(nameInput.offsetWidth).toBe(0); |
|||
|
|||
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); |
|||
userEvent.click(advancedFiltersBtn); |
|||
|
|||
expect(nameInput.offsetWidth).toBeGreaterThan(0); |
|||
|
|||
userEvent.type(nameInput, "fooo{backspace}"); |
|||
expect(nameInput.value).toBe("foo"); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: |
|||
|
|||
- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) |
|||
- [User Event](https://testing-library.com/docs/ecosystem-user-event) |
|||
- [Examples](https://github.com/testing-library/angular-testing-library/tree/master/apps/example-app/app/examples) |
|||
|
|||
### Clearing DOM After Each Spec |
|||
|
|||
One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so. |
|||
|
|||
We have prepared a simple function with which you can clear any leftover DOM elements after each test. |
|||
|
|||
```ts |
|||
// other imports |
|||
import { clearPage } from "@abp/ng.core/testing"; |
|||
|
|||
describe("MyComponent", () => { |
|||
let fixture: ComponentFixture<MyComponent>; |
|||
|
|||
afterEach(() => clearPage(fixture)); |
|||
|
|||
beforeEach(async () => { |
|||
const result = await render(MyComponent, { |
|||
/* removed for sake of brevity */ |
|||
}); |
|||
fixture = result.fixture; |
|||
}); |
|||
|
|||
// specs here |
|||
}); |
|||
``` |
|||
|
|||
Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike. |
|||
|
|||
### Waiting |
|||
|
|||
Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. |
|||
|
|||
For this purpose, we have prepared a `wait` function. |
|||
|
|||
```ts |
|||
// other imports |
|||
import { wait } from "@abp/ng.core/testing"; |
|||
|
|||
describe("MyComponent", () => { |
|||
beforeEach(/* removed for sake of brevity */); |
|||
|
|||
it("should open a modal", async () => { |
|||
const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); |
|||
userEvent.click(openModalBtn); |
|||
|
|||
await wait(fixture); |
|||
|
|||
const modal = screen.getByRole("dialog"); |
|||
|
|||
expect(modal).toBeTruthy(); |
|||
|
|||
/* wait again after closing the modal */ |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right. |
|||
|
|||
## Testing Example |
|||
|
|||
Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like. |
|||
|
|||
```ts |
|||
import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; |
|||
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; |
|||
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; |
|||
import { ComponentFixture } from "@angular/core/testing"; |
|||
import { |
|||
NgbCollapseModule, |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
} from "@ng-bootstrap/ng-bootstrap"; |
|||
import { NgxValidateCoreModule } from "@ngx-validate/core"; |
|||
import { CountryService } from "@proxy/countries"; |
|||
import { |
|||
findByText, |
|||
getByLabelText, |
|||
getByRole, |
|||
getByText, |
|||
queryByRole, |
|||
render, |
|||
screen, |
|||
} from "@testing-library/angular"; |
|||
import userEvent from "@testing-library/user-event"; |
|||
import { BehaviorSubject, of } from "rxjs"; |
|||
import { CountryComponent } from "./country.component"; |
|||
|
|||
const list$ = new BehaviorSubject({ |
|||
items: [{ id: "ID_US", name: "United States of America" }], |
|||
totalCount: 1, |
|||
}); |
|||
|
|||
describe("Country", () => { |
|||
let fixture: ComponentFixture<CountryComponent>; |
|||
|
|||
afterEach(() => clearPage(fixture)); |
|||
|
|||
beforeEach(async () => { |
|||
const result = await render(CountryComponent, { |
|||
imports: [ |
|||
CoreTestingModule.withConfig(), |
|||
ThemeSharedTestingModule.withConfig(), |
|||
ThemeBasicTestingModule.withConfig(), |
|||
NgxValidateCoreModule, |
|||
NgbCollapseModule, |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
], |
|||
providers: [ |
|||
{ |
|||
provide: CountryService, |
|||
useValue: { |
|||
getList: () => list$, |
|||
}, |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
fixture = result.fixture; |
|||
}); |
|||
|
|||
it("should display advanced filters", () => { |
|||
const filters = screen.getByTestId("country-filters"); |
|||
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; |
|||
expect(nameInput.offsetWidth).toBe(0); |
|||
|
|||
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); |
|||
userEvent.click(advancedFiltersBtn); |
|||
|
|||
expect(nameInput.offsetWidth).toBeGreaterThan(0); |
|||
|
|||
userEvent.type(nameInput, "fooo{backspace}"); |
|||
expect(nameInput.value).toBe("foo"); |
|||
|
|||
userEvent.click(advancedFiltersBtn); |
|||
expect(nameInput.offsetWidth).toBe(0); |
|||
}); |
|||
|
|||
it("should have a heading", () => { |
|||
const heading = screen.getByRole("heading", { name: "Countries" }); |
|||
expect(heading).toBeTruthy(); |
|||
}); |
|||
|
|||
it("should render list in table", async () => { |
|||
const table = await screen.findByTestId("country-table"); |
|||
|
|||
const name = getByText(table, "United States of America"); |
|||
expect(name).toBeTruthy(); |
|||
}); |
|||
|
|||
it("should display edit modal", async () => { |
|||
const actionsBtn = screen.queryByRole("button", { name: /actions/i }); |
|||
userEvent.click(actionsBtn); |
|||
|
|||
const editBtn = screen.getByRole("button", { name: /edit/i }); |
|||
userEvent.click(editBtn); |
|||
|
|||
await wait(fixture); |
|||
|
|||
const modal = screen.getByRole("dialog"); |
|||
const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); |
|||
expect(modalHeading).toBeTruthy(); |
|||
|
|||
const closeBtn = getByText(modal, "×"); |
|||
userEvent.click(closeBtn); |
|||
|
|||
await wait(fixture); |
|||
|
|||
expect(screen.queryByRole("dialog")).toBeFalsy(); |
|||
}); |
|||
|
|||
it("should display create modal", async () => { |
|||
const newBtn = screen.getByRole("button", { name: /new/i }); |
|||
userEvent.click(newBtn); |
|||
|
|||
await wait(fixture); |
|||
|
|||
const modal = screen.getByRole("dialog"); |
|||
const modalHeading = queryByRole(modal, "heading", { name: /new/i }); |
|||
|
|||
expect(modalHeading).toBeTruthy(); |
|||
}); |
|||
|
|||
it("should validate required name field", async () => { |
|||
const newBtn = screen.getByRole("button", { name: /new/i }); |
|||
userEvent.click(newBtn); |
|||
|
|||
await wait(fixture); |
|||
|
|||
const modal = screen.getByRole("dialog"); |
|||
const nameInput = getByRole(modal, "textbox", { |
|||
name: /^name/i, |
|||
}) as HTMLInputElement; |
|||
|
|||
userEvent.type(nameInput, "x"); |
|||
userEvent.type(nameInput, "{backspace}"); |
|||
|
|||
const nameError = await findByText(modal, /required/i); |
|||
expect(nameError).toBeTruthy(); |
|||
}); |
|||
|
|||
it("should delete a country", () => { |
|||
const getSpy = spyOn(fixture.componentInstance.list, "get"); |
|||
const deleteSpy = jasmine.createSpy().and.returnValue(of(null)); |
|||
fixture.componentInstance.service.delete = deleteSpy; |
|||
|
|||
const actionsBtn = screen.queryByRole("button", { name: /actions/i }); |
|||
userEvent.click(actionsBtn); |
|||
|
|||
const deleteBtn = screen.getByRole("button", { name: /delete/i }); |
|||
userEvent.click(deleteBtn); |
|||
|
|||
const confirmText = screen.getByText("AreYouSure"); |
|||
expect(confirmText).toBeTruthy(); |
|||
|
|||
const confirmBtn = screen.getByRole("button", { name: "Yes" }); |
|||
userEvent.click(confirmBtn); |
|||
|
|||
expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); |
|||
expect(getSpy).toHaveBeenCalledTimes(1); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
## CI Configuration |
|||
|
|||
You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below: |
|||
|
|||
```json |
|||
// angular.json |
|||
|
|||
"test": { |
|||
"builder": "@angular-devkit/build-angular:karma", |
|||
"options": { /* several options here */ }, |
|||
"configurations": { |
|||
"production": { |
|||
"karmaConfig": "karma.conf.prod.js" |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options. |
|||
|
|||
Finally, don't forget to run your CI tests with the following command: |
|||
|
|||
```sh |
|||
npm test -- --prod |
|||
``` |
|||
|
|||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 64 KiB |
@ -0,0 +1,13 @@ |
|||
using System.Globalization; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Client |
|||
{ |
|||
internal static class MvcCachedApplicationConfigurationClientHelper |
|||
{ |
|||
public static string CreateCacheKey(ICurrentUser currentUser) |
|||
{ |
|||
return $"ApplicationConfiguration_{currentUser.Id?.ToString("N") ?? "Anonymous"}_{CultureInfo.CurrentUICulture.Name}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Client |
|||
{ |
|||
public class MvcCurrentApplicationConfigurationCacheResetEventHandler : |
|||
ILocalEventHandler<CurrentApplicationConfigurationCacheResetEventData>, |
|||
ITransientDependency |
|||
{ |
|||
protected ICurrentUser CurrentUser { get; } |
|||
protected IDistributedCache<ApplicationConfigurationDto> Cache { get; } |
|||
|
|||
public MvcCurrentApplicationConfigurationCacheResetEventHandler(ICurrentUser currentUser, |
|||
IDistributedCache<ApplicationConfigurationDto> cache) |
|||
{ |
|||
CurrentUser = currentUser; |
|||
Cache = cache; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(CurrentApplicationConfigurationCacheResetEventData eventData) |
|||
{ |
|||
await Cache.RemoveAsync(CreateCacheKey()); |
|||
} |
|||
|
|||
protected virtual string CreateCacheKey() |
|||
{ |
|||
return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations |
|||
{ |
|||
/// <summary>
|
|||
/// This event is used to invalidate current user's cached configuration.
|
|||
/// </summary>
|
|||
public class CurrentApplicationConfigurationCacheResetEventData |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc |
|||
{ |
|||
internal static class AbpActionContextExtensions |
|||
{ |
|||
public static T GetRequiredService<T>(this FilterContext context) |
|||
where T : class |
|||
{ |
|||
return context.HttpContext.RequestServices.GetRequiredService<T>(); |
|||
} |
|||
|
|||
public static T GetService<T>(this FilterContext context, T defaultValue = default) |
|||
where T : class |
|||
{ |
|||
return context.HttpContext.RequestServices.GetService<T>() ?? defaultValue; |
|||
} |
|||
} |
|||
} |
|||
@ -1,55 +1,22 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc |
|||
{ |
|||
public abstract class AbpViewComponent : ViewComponent |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; set; } |
|||
protected readonly object ServiceProviderLock = new object(); |
|||
|
|||
protected TService LazyGetRequiredService<TService>(ref TService reference) |
|||
=> LazyGetRequiredService(typeof(TService), ref reference); |
|||
|
|||
protected TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
lock (ServiceProviderLock) |
|||
{ |
|||
if (reference == null) |
|||
{ |
|||
reference = (TRef)ServiceProvider.GetRequiredService(serviceType); |
|||
} |
|||
} |
|||
} |
|||
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } |
|||
|
|||
return reference; |
|||
} |
|||
public IServiceProvider ServiceProvider { get; set; } |
|||
|
|||
protected Type ObjectMapperContext { get; set; } |
|||
protected IObjectMapper ObjectMapper |
|||
{ |
|||
get |
|||
{ |
|||
if (_objectMapper != null) |
|||
{ |
|||
return _objectMapper; |
|||
} |
|||
|
|||
if (ObjectMapperContext == null) |
|||
{ |
|||
return LazyGetRequiredService(ref _objectMapper); |
|||
} |
|||
|
|||
return LazyGetRequiredService( |
|||
typeof(IObjectMapper<>).MakeGenericType(ObjectMapperContext), |
|||
ref _objectMapper |
|||
); |
|||
} |
|||
} |
|||
private IObjectMapper _objectMapper; |
|||
protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetService<IObjectMapper>(provider => |
|||
ObjectMapperContext == null |
|||
? provider.GetRequiredService<IObjectMapper>() |
|||
: (IObjectMapper) provider.GetRequiredService(typeof(IObjectMapper<>).MakeGenericType(ObjectMapperContext))); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
using System.Buffers; |
|||
using System.Text.Encodings.Web; |
|||
using System.Text.Json; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.Formatters; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.ObjectPool; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Json |
|||
{ |
|||
public class AbpHybridJsonOptionsSetup : IConfigureOptions<MvcOptions> |
|||
{ |
|||
private readonly IOptions<JsonOptions> _jsonOptions; |
|||
private readonly IOptions<MvcNewtonsoftJsonOptions> _mvcNewtonsoftJsonOptions; |
|||
private readonly ILoggerFactory _loggerFactory; |
|||
private readonly ArrayPool<char> _charPool; |
|||
private readonly ObjectPoolProvider _objectPoolProvider; |
|||
|
|||
public AbpHybridJsonOptionsSetup( |
|||
IOptions<JsonOptions> jsonOptions, |
|||
IOptions<MvcNewtonsoftJsonOptions> mvcNewtonsoftJsonOptions, |
|||
ILoggerFactory loggerFactory, |
|||
ArrayPool<char> charPool, |
|||
ObjectPoolProvider objectPoolProvider) |
|||
{ |
|||
_jsonOptions = jsonOptions; |
|||
_mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions; |
|||
_loggerFactory = loggerFactory; |
|||
_charPool = charPool; |
|||
_objectPoolProvider = objectPoolProvider; |
|||
} |
|||
|
|||
public void Configure(MvcOptions options) |
|||
{ |
|||
var systemTextJsonInputFormatter = new SystemTextJsonInputFormatter( |
|||
_jsonOptions.Value, |
|||
_loggerFactory.CreateLogger<SystemTextJsonInputFormatter>()); |
|||
|
|||
var newtonsoftJsonInputFormatter = new NewtonsoftJsonInputFormatter( |
|||
_loggerFactory.CreateLogger<NewtonsoftJsonInputFormatter>(), |
|||
_mvcNewtonsoftJsonOptions.Value.SerializerSettings, |
|||
_charPool, |
|||
_objectPoolProvider, |
|||
options, |
|||
_mvcNewtonsoftJsonOptions.Value); |
|||
|
|||
options.InputFormatters.RemoveType<SystemTextJsonInputFormatter>(); |
|||
options.InputFormatters.RemoveType<NewtonsoftJsonInputFormatter>(); |
|||
options.InputFormatters.Add(new AbpHybridJsonInputFormatter(systemTextJsonInputFormatter, newtonsoftJsonInputFormatter)); |
|||
|
|||
var jsonSerializerOptions = _jsonOptions.Value.JsonSerializerOptions; |
|||
if (jsonSerializerOptions.Encoder is null) |
|||
{ |
|||
// If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters.
|
|||
jsonSerializerOptions = new JsonSerializerOptions(jsonSerializerOptions) |
|||
{ |
|||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
|||
}; |
|||
} |
|||
|
|||
var systemTextJsonOutputFormatter = new SystemTextJsonOutputFormatter(jsonSerializerOptions); |
|||
var newtonsoftJsonOutputFormatter = new NewtonsoftJsonOutputFormatter( |
|||
_mvcNewtonsoftJsonOptions.Value.SerializerSettings, |
|||
_charPool, |
|||
options); |
|||
|
|||
options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>(); |
|||
options.OutputFormatters.RemoveType<NewtonsoftJsonOutputFormatter>(); |
|||
options.OutputFormatters.Add(new AbpHybridJsonOutputFormatter(systemTextJsonOutputFormatter, newtonsoftJsonOutputFormatter)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"culture": "nl", |
|||
"texts": { |
|||
"Volo.Authorization:010001": "Autorisatie mislukt! De benodigde policy is niet aan u verleend.", |
|||
"Volo.Authorization:010002": "Autorisatie mislukt! De benodigde policy: '{PolicyName}' is niet aan u verleend.", |
|||
"Volo.Authorization:010003": "Autorisatie mislukt! De benodigde policy is niet aan de opgegeven resource: '{ResourceName}' verleend.", |
|||
"Volo.Authorization:010004": "Autorisatie mislukt! De benodigde requirement is niet aan de opgegeven resource: '{ResourceName}' verleend.", |
|||
"Volo.Authorization:010005": "Autorisatie mislukt! De benodigde requirements zijn niet aan de opgegeven resource: '{ResourceName}' verleend." |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue