@ -1,3 +1,3 @@ |
|||
## Contribution |
|||
|
|||
See the [contribution guide](docs/en/Contribution/Index.md). |
|||
The contribution guide is available at [contribution guide](docs/en/contribution/index.md). |
|||
|
|||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 44 KiB |
@ -0,0 +1,94 @@ |
|||
# .NET 9 Performance Improvements Summary |
|||
|
|||
With every release, .NET becomes faster & faster! You get these improvements for free by just updating your project to the latest .NET! |
|||
|
|||
 |
|||
|
|||
It’s very interesting that **20% of these improvements** are implemented by **open-source volunteers** rather than Microsoft employees. These improvements mostly focus on cloud-native and high-throughput applications. I’ll briefly list them below. |
|||
|
|||
|
|||
|
|||
## 1. Dynamic PGO with JIT Compiler |
|||
|
|||
* ### What is dynamic PGO? |
|||
With “Profile Guided Optimization” the compiler optimizes the code, based on the flow and the way the code executes. It is predicated on the idea that every potential behavior of the code will always transpire. |
|||
|
|||
* ### What’s Improved? |
|||
The tiered compilation, inlining, and dynamic PGO are three ways that .NET 9 optimizes the JIT compiler. This enhances runtime performance and speeds up the time for apps to launch. |
|||
|
|||
* ### Performance Gains |
|||
CPU use is lower during execution; therefore, **startup times are about 15% faster**. |
|||
|
|||
* ### As a Developer |
|||
Faster, smoother deployments with reduced warm-up times... These enhancements reduce latency for applications with complex workflows, particularly in microservices and high-throughput environments. |
|||
|
|||
* ### How to activate Dynamic PGO? |
|||
Add the following to your `csproj` file, or if you have several `csproj` files, you can add it once in `Directory.Build.props` file. Check out [this link](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/compilation#profile-guided-optimization) to understand PGO. |
|||
|
|||
```xml |
|||
<PropertyGroup> |
|||
<TieredPGO>true</TieredPGO> |
|||
</PropertyGroup> |
|||
``` |
|||
|
|||
|
|||
|
|||
## 2. Library Improvements |
|||
|
|||
* ### What’s Improved? |
|||
|
|||
LINQ and JSON serialization, collections and libraries are significantly improved with .NET 9. |
|||
|
|||
* ### Performance Gains |
|||
|
|||
**JSON serialization** performance **increases by about 35%**. This helps with heavy data parsing and API requests. Less memory is allocated to `Span` operations as well, and LINQ techniques such as `Where` and `Select` are now faster. |
|||
|
|||
* ### As a Developer |
|||
|
|||
This means that apps will be faster, especially those that handle data primarily in JSON or manipulate data with LINQ. |
|||
|
|||
|
|||
|
|||
## 3. ASP.NET Core |
|||
|
|||
* ### What’s Improved? |
|||
Kestrel server has undergone significant modifications, mostly in processing the HTTP/2 and HTTP/3 protocols. |
|||
|
|||
* ### Performance Gains |
|||
Now, **Kestrel handles requests up to 20% faster** and **has a 25% reduction in average latency**. Improved connection management and SSL processing also result in overall efficiency gains. |
|||
|
|||
* ### As a Developer |
|||
These modifications result in less resource use, quicker response times for web applications, and more seamless scaling in high-traffic situations. |
|||
|
|||
|
|||
|
|||
## 4. Garbage Collection & Memory Management |
|||
|
|||
* ### What’s Improved? |
|||
NET 9’s garbage collection (GC) is more effective, especially for apps with high allocation rates. |
|||
|
|||
* ### Performance Gains |
|||
Applications experience smoother **garbage collection cycles with 8–12% less memory overhead**, which lowers latency and delays. |
|||
|
|||
* ### As a Developer |
|||
The performance will be more reliable and predictable for developers as there will be fewer memory-related bottlenecks, particularly in applications that involve frequent object allocations. |
|||
|
|||
|
|||
|
|||
## 5. Native AOT Compilation |
|||
|
|||
* ### What’s Improved? |
|||
Native AOT (Ahead-of-Time) compilation is now more efficient by lowering memory footprint and cold-start times. This leads to better support for cloud-native applications. |
|||
|
|||
* ### Performance Gains |
|||
Native AOT apps now have faster cold launches and use **30–40% less memory**. This improvement focuses on containerized applications. |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
**References:** |
|||
|
|||
* [Microsoft .NET blog post](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/). |
|||
* [What’s new in the .NET 9 runtime?](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#performance-improvements) |
|||
|
|||
|
After Width: | Height: | Size: 441 KiB |
@ -0,0 +1,143 @@ |
|||
# Entity Filters |
|||
|
|||
Every CRUD page includes some sort of inputs to filter the listed data. Some of the inputs are common among all of the entities like the `Search` box. In addition, every entity has its own advanced filters depending on its fields. To reduce the amount of code written on every CRUD page, the Angular UI of ABP Commercial introduces a new type of component called `abp-advanced-entity-filters` |
|||
|
|||
## Setup |
|||
|
|||
The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the entity filter components. |
|||
You can either import the `CommercialUiModule` which contains other components as well as `AdvancedEntityFilters` or you can directly import the `AdvancedEntityFiltersModule` if you do not need other components. Here is how you import them in your Angular module: |
|||
|
|||
```javascript |
|||
import { |
|||
CommercialUiModule, |
|||
AdvancedEntityFiltersModule, |
|||
} from "@volo/abp.commercial.ng.ui"; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
CommercialUiModule, |
|||
|
|||
// OR |
|||
|
|||
AdvancedEntityFiltersModule, |
|||
], |
|||
// rest of the module metadata |
|||
}) |
|||
export class YourModule {} |
|||
``` |
|||
|
|||
## Usage |
|||
|
|||
Let's take a look at the `Users` page from the `Identity` module. |
|||
|
|||
 |
|||
|
|||
As shown in the screenshot, `abp-advanced-entity-filters` usually contain two parts, an entity filter (common among entities), i.e. `abp-entity-filter`, and entity-specific filters which are encapsulated within the `abp-advanced-entity-filters-form` component. |
|||
|
|||
`users.component.html` |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-form> |
|||
<form #filterForm (keyup.enter)="list.get()"> |
|||
<div class="row"> |
|||
<!-- Form elements are omitted --> |
|||
|
|||
<div class="col-12 col-sm-auto align-self-end mb-3"> |
|||
<div class="row"> |
|||
<div class="col-6 col-sm-auto d-grid"> |
|||
<button |
|||
type="button" |
|||
class="btn btn-outline-primary" |
|||
(click)="clearFilters()" |
|||
> |
|||
<span>{%{{{ 'AbpUi::Clear' | abpLocalization }}}%}</span> |
|||
</button> |
|||
</div> |
|||
<div class="col-6 col-sm-auto d-grid"> |
|||
<button |
|||
type="button" |
|||
class="btn btn-primary" |
|||
(click)="list.get()" |
|||
> |
|||
<span>{%{{{ 'AbpUi::Refresh' | abpLocalization }}}%}</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
The `abp-advanced-entity-filters` already contains the `abp-entity-filter` component so you do not need to pass it. However, the `abp-entity-filter` component needs an instance of `ListService` which is usually stored in the `list` field of the page. You can also change the placeholder of the component via `entityFilterPlaceholder` input which is passed into the `abpLocalization` pipe so that it uses the translated text. Default is `'AbpUi::PagerSearch'` |
|||
|
|||
E.g |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters |
|||
[list]="list" |
|||
entityFilterPlaceholder="AbpUi::PagerSearch" |
|||
> |
|||
<!-- ... --> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
### Inputs |
|||
|
|||
- `list`: an instance of `ListService` |
|||
- `entityFilterPlaceholder`: the placeholder of `abp-entity-filter` component. Default: `'AbpUi::PagerSearch'` |
|||
- `localizationSourceName`: the localization source of the current page. E.g: `AbpIdentity` |
|||
|
|||
### Inner components |
|||
|
|||
Some entities are simple and do not require any filter other than the `abp-entity-filter`. In this case, you can simply use the `abp-advanced-entity-filters` without anything in between. |
|||
|
|||
E.g. |
|||
|
|||
Let's remove `form` from the `Users` page |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
|||
If your component needs other filters, you can pass your own `form` within the `abp-advanced-entity-filters-form` component. This will render your form as well as a toggle (`abp-advanced-entity-filters-toggle`) to show and hide the form |
|||
|
|||
E.g. |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-form> |
|||
<form> |
|||
<!-- Content is omitted for sake of simplicity --> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
|||
Last but not least, if you need to render some content above the `abp-entity-filter` component, you can use the `abp-advanced-entity-filters-above-search`. |
|||
|
|||
E.g. |
|||
|
|||
```html |
|||
<abp-advanced-entity-filters [list]="list" localizationSourceName="AbpIdentity"> |
|||
<abp-advanced-entity-filters-above-search> |
|||
<h3>Custom Content above entity-filter</h3> |
|||
</abp-advanced-entity-filters-above-search> |
|||
|
|||
<abp-advanced-entity-filters-form> |
|||
<form> |
|||
<!-- Content is omitted for sake of simplicity --> |
|||
</form> |
|||
</abp-advanced-entity-filters-form> |
|||
</abp-advanced-entity-filters> |
|||
``` |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -0,0 +1,98 @@ |
|||
# Lookup Components |
|||
|
|||
The Angular UI of ABP Commercial introduces some components with `abp-lookup-...` selector prefix. These components are used for retrieving relational entity data. |
|||
|
|||
## Setup |
|||
|
|||
The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. Here is how you import them in your Angular module: |
|||
|
|||
```javascript |
|||
import { CommercialUiModule } from '@volo/abp.commercial.ng.ui'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
// other imports |
|||
CommercialUiModule, |
|||
], |
|||
// rest of the module metadata |
|||
}) |
|||
export class YourModule {} |
|||
``` |
|||
|
|||
Now you can use the lookup components in your components declared by this module. |
|||
|
|||
## Lookup HTTP Requests |
|||
|
|||
The lookup requests are used by all lookup components to get the related entity records. Because of lexical this, _they must be arrow functions_. |
|||
|
|||
```javascript |
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class AuthorService { |
|||
getCountryLookup = (input: LookupRequestDto) => |
|||
this.restService.request<any, PagedResultDto<LookupDto<string>>>({ |
|||
method: 'GET', |
|||
url: '/api/app/authors/country-lookup', |
|||
params: { filter: input.filter, skipCount: input.skipCount, maxResultCount: input.maxResultCount }, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
// rest of the service is removed for brevity |
|||
} |
|||
``` |
|||
|
|||
## Lookup Typeahead Component |
|||
|
|||
Typeahead is a good choice when you have an unknown number of records for the related entity or you want to improve the UX with a search ability. Although not the best scenario, the country picker below shows how the lookup typeahead works: |
|||
|
|||
 |
|||
|
|||
Here is how it is used in the template. |
|||
|
|||
```html |
|||
<abp-lookup-typeahead |
|||
cid="author-country-id" |
|||
formControlName="countryId" |
|||
displayNameProp="name" |
|||
[editingData]="selected?.country" |
|||
[getFn]="service.getCountryLookup" |
|||
></abp-lookup-typeahead> |
|||
``` |
|||
|
|||
The available properties are as follows: |
|||
|
|||
- **cid:** The id of the form control (e.g. an input or a select element) inside the lookup component. Lets form controls respond to `<label>` events. |
|||
- **editingData:** The related entity data if a record is being updated. |
|||
- **displayNameProp:** The property of the updated record to use as a display name in the form control. |
|||
- **lookupNameProp:** The property of the entity to use as a display name in options. Should macth the lookup HTTP request interface. _(default: displayName)_ |
|||
- **lookupIdProp:** The property of the entity to use as the unique key in options. Should macth the lookup HTTP request interface. _(default: id)_ |
|||
- **maxResultCount:** The maximum number of options to display. _(default: 10)_ |
|||
- **getFn:** A function to get the related entity records with HTTP requests. Because of lexical this, _it must be a an arrow function_. |
|||
- **disabled:** This property lets you disable/enable a lookup component. _(default: false)_. |
|||
|
|||
## Lookup Select Component |
|||
|
|||
Select is a good choice when you have a low (and usually fixed) number of records for the related entity and search is not necessary. The country picker below shows how the lookup select works: |
|||
|
|||
 |
|||
|
|||
Here is how it is used in the template. |
|||
|
|||
```html |
|||
<abp-lookup-select |
|||
cid="author-country-id" |
|||
formControlName="countryId" |
|||
displayNameProp="name" |
|||
[getFn]="service.getCountryLookup" |
|||
></abp-lookup-select> |
|||
``` |
|||
|
|||
The available properties are as follows: |
|||
|
|||
- **cid:** The id of the form control (e.g. an input or a select element) inside the lookup component. Lets form controls respond to `<label>` events. |
|||
- **displayNameProp:** The property of the updated record to use as a display name in the form control. |
|||
- **lookupNameProp:** The property of the entity to use as a display name in options. Should macth the lookup HTTP request interface. _(default: displayName)_ |
|||
- **lookupIdProp:** The property of the entity to use as the unique key in options. Should macth the lookup HTTP request interface. _(default: id)_ |
|||
- **getFn:** A function to get the related entity records with HTTP requests. Because of lexical this, _it must be a an arrow function_. |
|||
- **disabled:** This property lets you disable/enable a lookup component. _(default: false)_. |
|||
@ -0,0 +1,77 @@ |
|||
# Manage Profile Page Tabs |
|||
|
|||
 |
|||
|
|||
The tabs in the manage profile page can be managed via `ManageProfileTabsService` which is exposed by the `@volo/abp.ng.account/public/config` package. You can add, remove, or edit a tab with using this service. |
|||
|
|||
See the example below, covers all features: |
|||
|
|||
```ts |
|||
// manage-profile-tabs.provider.ts |
|||
|
|||
import { APP_INITIALIZER, Component } from "@angular/core"; |
|||
import { TwoFactorTabComponent } from "@volo/abp.ng.account/public"; |
|||
import { |
|||
eAccountManageProfileTabNames, |
|||
ManageProfileTabsService, |
|||
} from "@volo/abp.ng.account/public/config"; |
|||
import { MyAwesomeTabComponent } from "./my-awesome-tab/my-awesome-tab.component"; |
|||
|
|||
@Component({ |
|||
standalone: true, |
|||
selector: "abp-my-awesome-tab", |
|||
template: `My Awesome Tab`, |
|||
}) |
|||
class MyAwesomeTabComponent {} |
|||
|
|||
export const MANAGE_PROFILE_TAB_PROVIDER = { |
|||
provide: APP_INITIALIZER, |
|||
useFactory: configureManageProfileTabs, |
|||
deps: [ManageProfileTabsService], |
|||
multi: true, |
|||
}; |
|||
|
|||
export function configureManageProfileTabs(tabs: ManageProfileTabsService) { |
|||
return () => { |
|||
tabs.add([ |
|||
{ |
|||
name: "::MyAwesomeTab", // supports localization keys |
|||
order: 5, |
|||
component: MyAwesomeTabComponent, |
|||
}, |
|||
]); |
|||
|
|||
tabs.patch(eAccountManageProfileTabNames.TwoFactor, { |
|||
name: "Two factor authentication", |
|||
component: TwoFactorTabComponent, |
|||
}); |
|||
|
|||
tabs.remove([eAccountManageProfileTabNames.ProfilePicture]); |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```ts |
|||
//app.module.ts |
|||
|
|||
import { MANAGE_PROFILE_TAB_PROVIDER } from "./manage-profile-tabs.provider"; |
|||
|
|||
@NgModule({ |
|||
providers: [MANAGE_PROFILE_TAB_PROVIDER], |
|||
}) |
|||
export class AppModule {} |
|||
``` |
|||
|
|||
What we have done above; |
|||
|
|||
- Created the `manage-profile-page-tabs.provider.ts`. |
|||
- Determined the `configureManageProfileTabs` function to perform manage profile tabs actions. |
|||
- Added a new tab labeled "My awesome tab". |
|||
- Renamed the "Two factor" tab label. |
|||
- Removed the "Profile picture" tab. |
|||
- Determined the `MANAGE_PROFILE_TAB_PROVIDER` to be able to run the `configureManageProfileTabs` function on initialization. |
|||
- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `AppModule` providers. |
|||
|
|||
See the result: |
|||
|
|||
 |
|||
@ -0,0 +1,14 @@ |
|||
# Angular UI |
|||
|
|||
[Angular](https://angular.dev/) is a framework for building interactive, client-side web UIs using [TypeScript](https://www.typescriptlang.org) and [NodeJS](https://nodejs.org). |
|||
|
|||
ABP Angular provides the infrastructure to communicate with the ABP backend and offers utilities to simplify frontend development. We’ll explore and dive into the details under the following main topics: |
|||
|
|||
- Development |
|||
- Core Functionality |
|||
- Utilities |
|||
- Customization |
|||
- Components |
|||
|
|||
|
|||
You can also make a [Quick Start](./quick-start.md) |
|||
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,96 @@ |
|||
// <auto-generated/>
|
|||
#pragma warning disable 1591
|
|||
namespace Volo.Abp.AspNetCore.RazorViews |
|||
{ |
|||
#line hidden
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
#nullable restore |
|||
#line 1 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using System.Globalization; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
#nullable restore |
|||
#line 2 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.MultiTenancy.Views; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
#nullable restore |
|||
#line 3 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
internal class MultiTenancyMiddlewareErrorPage : AbpCompilationRazorPageBase |
|||
{ |
|||
#pragma warning disable 1998
|
|||
public async override global::System.Threading.Tasks.Task ExecuteAsync() |
|||
{ |
|||
#nullable restore |
|||
#line 5 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
|
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 404; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("\n"); |
|||
WriteLiteral("\n<html"); |
|||
BeginWriteAttribute("lang", " lang=\"", 453, "\"", 512, 1); |
|||
#nullable restore |
|||
#line 19 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name), 460, 52, false); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
EndWriteAttribute(); |
|||
WriteLiteral(">\n <head>\n <meta charset=\"utf-8\" />\n <title>"); |
|||
#nullable restore |
|||
#line 22 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Message)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("</title>\n </head>\n <body>\n <h3>"); |
|||
#nullable restore |
|||
#line 25 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Message)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("</h3>\n <p>"); |
|||
#nullable restore |
|||
#line 26 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
Write(HtmlEncoder.Encode(Model.Details)); |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral("<p/>\n </body>\n</html>\n"); |
|||
} |
|||
#pragma warning restore 1998
|
|||
#nullable restore |
|||
#line 10 "MultiTenancyMiddlewareErrorPage.cshtml"
|
|||
|
|||
public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model) |
|||
{ |
|||
Model = model; |
|||
} |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel Model { get; set; } |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
} |
|||
} |
|||
#pragma warning restore 1591
|
|||
@ -0,0 +1,28 @@ |
|||
@using System.Globalization |
|||
@using Volo.Abp.AspNetCore.MultiTenancy.Views |
|||
@using Volo.Abp.AspNetCore.RazorViews |
|||
@inherits AbpCompilationRazorPageBase |
|||
@{ |
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 404; |
|||
} |
|||
|
|||
@functions{ |
|||
public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model) |
|||
{ |
|||
Model = model; |
|||
} |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel Model { get; set; } |
|||
} |
|||
|
|||
<html lang="@HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>@HtmlEncoder.Encode(Model.Message)</title> |
|||
</head> |
|||
<body> |
|||
<h3>@HtmlEncoder.Encode(Model.Message)</h3> |
|||
<p>@HtmlEncoder.Encode(Model.Details)<p/> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,14 @@ |
|||
namespace Volo.Abp.AspNetCore.MultiTenancy.Views; |
|||
|
|||
public class MultiTenancyMiddlewareErrorPageModel |
|||
{ |
|||
public string Message { get; set; } |
|||
|
|||
public string Details { get; set; } |
|||
|
|||
public MultiTenancyMiddlewareErrorPageModel(string message, string details) |
|||
{ |
|||
Message = message; |
|||
Details = details; |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
// <auto-generated/>
|
|||
#pragma warning disable 1591
|
|||
namespace Volo.Abp.AspNetCore.RazorViews |
|||
{ |
|||
#line hidden
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
#nullable restore |
|||
#line 1 "AbpMvcLibsErrorPage.cshtml"
|
|||
using Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
internal class AbpMvcLibsErrorPage : AbpCompilationRazorPageBase |
|||
{ |
|||
#pragma warning disable 1998
|
|||
public async override global::System.Threading.Tasks.Task ExecuteAsync() |
|||
{ |
|||
#nullable restore |
|||
#line 3 "AbpMvcLibsErrorPage.cshtml"
|
|||
|
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 500; |
|||
|
|||
#line default
|
|||
#line hidden
|
|||
#nullable disable |
|||
WriteLiteral(@"
|
|||
<html> |
|||
<head> |
|||
<meta charset=""utf-8"" /> |
|||
<title>Error - The Libs folder is missing!</title> |
|||
</head> |
|||
<body> |
|||
<h1> ⚠️ The Libs folder under the <code style=""background-color: #e7e7e7;"">wwwroot/libs</code> directory is empty!</h1> |
|||
|
|||
<p>The Libs folder contains mandatory NPM Packages for running the project.</p> |
|||
|
|||
<p>Make sure you run the <code style=""background-color: #e7e7e7;"">abp install-libs</code> CLI tool command.</p> |
|||
|
|||
<p>For more information, check out the <a href=""https://abp.io/docs/latest/CLI#install-libs"">ABP CLI documentation</a></p>
|
|||
</body> |
|||
</html> |
|||
");
|
|||
} |
|||
#pragma warning restore 1998
|
|||
} |
|||
} |
|||
#pragma warning restore 1591
|
|||
@ -0,0 +1,22 @@ |
|||
@using Volo.Abp.AspNetCore.RazorViews |
|||
@inherits AbpCompilationRazorPageBase |
|||
@{ |
|||
Response.ContentType = "text/html; charset=utf-8"; |
|||
Response.StatusCode = 500; |
|||
} |
|||
|
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>Error - The Libs folder is missing!</title> |
|||
</head> |
|||
<body> |
|||
<h1> ⚠️ The Libs folder under the <code style="background-color: #e7e7e7;">wwwroot/libs</code> directory is empty!</h1> |
|||
|
|||
<p>The Libs folder contains mandatory NPM Packages for running the project.</p> |
|||
|
|||
<p>Make sure you run the <code style="background-color: #e7e7e7;">abp install-libs</code> CLI tool command.</p> |
|||
|
|||
<p>For more information, check out the <a href="https://abp.io/docs/latest/CLI#install-libs">ABP CLI documentation</a></p> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,283 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Text.Encodings.Web; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public abstract class AbpCompilationRazorPageBase |
|||
{ |
|||
private readonly static Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); |
|||
private readonly static char[] NewLineChars = new[] { '\r', '\n' }; |
|||
private readonly Stack<TextWriter> _textWriterStack = new Stack<TextWriter>(); |
|||
|
|||
/// <summary>
|
|||
/// The request context
|
|||
/// </summary>
|
|||
protected HttpContext Context { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The request
|
|||
/// </summary>
|
|||
protected HttpRequest Request { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The response
|
|||
/// </summary>
|
|||
protected HttpResponse Response { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The output stream
|
|||
/// </summary>
|
|||
protected TextWriter Output { get; private set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// Html encoder used to encode content.
|
|||
/// </summary>
|
|||
protected HtmlEncoder HtmlEncoder { get; set; } = HtmlEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// Url encoder used to encode content.
|
|||
/// </summary>
|
|||
protected UrlEncoder UrlEncoder { get; set; } = UrlEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// JavaScript encoder used to encode content.
|
|||
/// </summary>
|
|||
protected JavaScriptEncoder JavaScriptEncoder { get; set; } = JavaScriptEncoder.Default; |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream to write to</param>
|
|||
public async Task ExecuteAsync(Stream stream) |
|||
{ |
|||
// We technically don't need this intermediate buffer if this method accepts a memory stream.
|
|||
var buffer = new MemoryStream(); |
|||
Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); |
|||
await ExecuteAsync(); |
|||
await Output.FlushAsync(); |
|||
await Output.DisposeAsync(); |
|||
buffer.Seek(0, SeekOrigin.Begin); |
|||
await buffer.CopyToAsync(stream); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
/// <param name="context"></param>
|
|||
public async Task ExecuteAsync(HttpContext context) |
|||
{ |
|||
Context = context; |
|||
Request = Context.Request; |
|||
Response = Context.Response; |
|||
var buffer = new MemoryStream(); |
|||
Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); |
|||
await ExecuteAsync(); |
|||
await Output.FlushAsync(); |
|||
await Output.DisposeAsync(); |
|||
buffer.Seek(0, SeekOrigin.Begin); |
|||
await buffer.CopyToAsync(Response.Body); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute an individual request
|
|||
/// </summary>
|
|||
public abstract Task ExecuteAsync(); |
|||
|
|||
protected virtual void PushWriter(TextWriter writer) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(writer); |
|||
|
|||
_textWriterStack.Push(Output); |
|||
Output = writer; |
|||
} |
|||
|
|||
protected virtual TextWriter PopWriter() |
|||
{ |
|||
Output = _textWriterStack.Pop(); |
|||
return Output; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="object"/> to write.</param>
|
|||
protected void WriteLiteral(object value) |
|||
{ |
|||
WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the given value without HTML encoding directly to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="string"/> to write.</param>
|
|||
protected void WriteLiteral(string? value) |
|||
{ |
|||
if (!string.IsNullOrEmpty(value)) |
|||
{ |
|||
Output.Write(value); |
|||
} |
|||
} |
|||
|
|||
private List<string>? AttributeValues { get; set; } |
|||
|
|||
protected void WriteAttributeValue(string thingy, int startPostion, object value, int endValue, int dealyo, bool yesno) |
|||
{ |
|||
if (AttributeValues == null) |
|||
{ |
|||
AttributeValues = new List<string>(); |
|||
} |
|||
|
|||
AttributeValues.Add(value.ToString()!); |
|||
} |
|||
|
|||
private string? AttributeEnding { get; set; } |
|||
|
|||
protected void BeginWriteAttribute(string name, string beginning, int startPosition, string ending, int endPosition, int thingy) |
|||
{ |
|||
Debug.Assert(string.IsNullOrEmpty(AttributeEnding)); |
|||
|
|||
Output.Write(beginning); |
|||
AttributeEnding = ending; |
|||
} |
|||
|
|||
protected void EndWriteAttribute() |
|||
{ |
|||
Debug.Assert(AttributeValues != null); |
|||
Debug.Assert(!string.IsNullOrEmpty(AttributeEnding)); |
|||
|
|||
var attributes = string.Join(" ", AttributeValues); |
|||
Output.Write(attributes); |
|||
AttributeValues = null; |
|||
|
|||
Output.Write(AttributeEnding); |
|||
AttributeEnding = null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the given attribute to the given writer
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the attribute to write</param>
|
|||
/// <param name="leader">The value of the prefix</param>
|
|||
/// <param name="trailer">The value of the suffix</param>
|
|||
/// <param name="values">The <see cref="AttributeValue"/>s to write.</param>
|
|||
protected void WriteAttribute( |
|||
string name, |
|||
string leader, |
|||
string trailer, |
|||
params AttributeValue[] values) |
|||
{ |
|||
ArgumentNullException.ThrowIfNull(name); |
|||
ArgumentNullException.ThrowIfNull(leader); |
|||
ArgumentNullException.ThrowIfNull(trailer); |
|||
|
|||
WriteLiteral(leader); |
|||
foreach (var value in values) |
|||
{ |
|||
WriteLiteral(value.Prefix); |
|||
|
|||
// The special cases here are that the value we're writing might already be a string, or that the
|
|||
// value might be a bool. If the value is the bool 'true' we want to write the attribute name
|
|||
// instead of the string 'true'. If the value is the bool 'false' we don't want to write anything.
|
|||
// Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself.
|
|||
string? stringValue; |
|||
if (value.Value is bool) |
|||
{ |
|||
if ((bool)value.Value) |
|||
{ |
|||
stringValue = name; |
|||
} |
|||
else |
|||
{ |
|||
continue; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
stringValue = value.Value as string; |
|||
} |
|||
|
|||
// Call the WriteTo(string) overload when possible
|
|||
if (value.Literal && stringValue != null) |
|||
{ |
|||
WriteLiteral(stringValue); |
|||
} |
|||
else if (value.Literal) |
|||
{ |
|||
WriteLiteral(value.Value); |
|||
} |
|||
else if (stringValue != null) |
|||
{ |
|||
Write(stringValue); |
|||
} |
|||
else |
|||
{ |
|||
Write(value.Value); |
|||
} |
|||
} |
|||
WriteLiteral(trailer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked
|
|||
/// </summary>
|
|||
/// <param name="result">The <see cref="HelperResult"/> to invoke</param>
|
|||
protected void Write(HelperResult result) |
|||
{ |
|||
result.WriteTo(Output); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the specified <paramref name="value"/> to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="object"/> to write.</param>
|
|||
/// <remarks>
|
|||
/// <see cref="HelperResult.WriteTo(TextWriter)"/> is invoked for <see cref="HelperResult"/> types.
|
|||
/// For all other types, the encoded result of <see cref="object.ToString"/> is written to
|
|||
/// <see cref="Output"/>.
|
|||
/// </remarks>
|
|||
protected void Write(object value) |
|||
{ |
|||
if (value is HelperResult helperResult) |
|||
{ |
|||
helperResult.WriteTo(Output); |
|||
} |
|||
else |
|||
{ |
|||
Write(Convert.ToString(value, CultureInfo.InvariantCulture)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The <see cref="string"/> to write.</param>
|
|||
protected void Write(string? value) |
|||
{ |
|||
if (!string.IsNullOrEmpty(value)) |
|||
{ |
|||
WriteLiteral(HtmlEncoder.Encode(value)); |
|||
} |
|||
} |
|||
|
|||
protected string HtmlEncodeAndReplaceLineBreaks(string input) |
|||
{ |
|||
if (string.IsNullOrEmpty(input)) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
// Split on line breaks before passing it through the encoder.
|
|||
return string.Join("<br />" + Environment.NewLine, |
|||
input.Split("\r\n", StringSplitOptions.None) |
|||
.SelectMany(s => s.Split(NewLineChars, StringSplitOptions.None)) |
|||
.Select(HtmlEncoder.Encode)); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public class AttributeValue |
|||
{ |
|||
public AttributeValue(string prefix, object value, bool literal) |
|||
{ |
|||
Prefix = prefix; |
|||
Value = value; |
|||
Literal = literal; |
|||
} |
|||
|
|||
public string Prefix { get; } |
|||
|
|||
public object Value { get; } |
|||
|
|||
public bool Literal { get; } |
|||
|
|||
public static AttributeValue FromTuple(Tuple<string, object, bool> value) |
|||
{ |
|||
return new AttributeValue(value.Item1, value.Item2, value.Item3); |
|||
} |
|||
|
|||
public static AttributeValue FromTuple(Tuple<string, string, bool> value) |
|||
{ |
|||
return new AttributeValue(value.Item1, value.Item2, value.Item3); |
|||
} |
|||
|
|||
public static implicit operator AttributeValue(Tuple<string, object, bool> value) |
|||
{ |
|||
return FromTuple(value); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace Volo.Abp.AspNetCore.RazorViews; |
|||
|
|||
public class HelperResult |
|||
{ |
|||
public HelperResult(Action<TextWriter> action) |
|||
{ |
|||
WriteAction = action; |
|||
} |
|||
|
|||
public Action<TextWriter> WriteAction { get; } |
|||
|
|||
public void WriteTo(TextWriter writer) |
|||
{ |
|||
WriteAction(writer); |
|||
} |
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Razor.Language; |
|||
using Microsoft.AspNetCore.Razor.Language.Extensions; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.Cli.Args; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands; |
|||
|
|||
public class GenerateRazorPage : IConsoleCommand, ITransientDependency |
|||
{ |
|||
public const string Name = "generate-razor-page"; |
|||
|
|||
public ILogger<GenerateRazorPage> Logger { get; set; } |
|||
|
|||
public GenerateRazorPage() |
|||
{ |
|||
Logger = NullLogger<GenerateRazorPage>.Instance; |
|||
} |
|||
|
|||
public Task ExecuteAsync(CommandLineArgs commandLineArgs) |
|||
{ |
|||
var targetProjectDirectory = Directory.GetCurrentDirectory(); |
|||
var projectEngine = CreateProjectEngine(targetProjectDirectory); |
|||
|
|||
var results = MainCore(projectEngine, targetProjectDirectory); |
|||
|
|||
foreach (var result in results) |
|||
{ |
|||
File.WriteAllText(result.FilePath, result.GeneratedCode); |
|||
} |
|||
|
|||
Logger.LogInformation($"{results.Count} files successfully generated."); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public string GetUsageInfo() |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
|
|||
sb.AppendLine(""); |
|||
sb.AppendLine("Usage:"); |
|||
sb.AppendLine("abp generate-razor-page"); |
|||
sb.AppendLine(""); |
|||
sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli"); |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
|
|||
public string GetShortDescription() |
|||
{ |
|||
return "Generates code files for Razor page."; |
|||
} |
|||
|
|||
private RazorProjectEngine CreateProjectEngine(string targetProjectDirectory, Action<RazorProjectEngineBuilder>? configure = null) |
|||
{ |
|||
var fileSystem = RazorProjectFileSystem.Create(targetProjectDirectory); |
|||
var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => |
|||
{ |
|||
builder |
|||
.SetNamespace("Volo.Abp.AspNetCore.RazorViews") |
|||
.ConfigureClass((document, @class) => |
|||
{ |
|||
@class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath); |
|||
@class.Modifiers.Clear(); |
|||
@class.Modifiers.Add("internal"); |
|||
}); |
|||
|
|||
SectionDirective.Register(builder); |
|||
|
|||
builder.Features.Add(new SuppressChecksumOptionsFeature()); |
|||
builder.Features.Add(new SuppressMetadataAttributesFeature()); |
|||
|
|||
if (configure != null) |
|||
{ |
|||
configure(builder); |
|||
} |
|||
|
|||
builder.AddDefaultImports(@"
|
|||
@using System |
|||
@using System.Threading.Tasks");
|
|||
}); |
|||
|
|||
return projectEngine; |
|||
} |
|||
|
|||
private List<RazorPageGeneratorResult> MainCore(RazorProjectEngine projectEngine, string targetProjectDirectory) |
|||
{ |
|||
var results = new List<RazorPageGeneratorResult>(); |
|||
Logger.LogInformation("Generating code files for pages in {0}", targetProjectDirectory); |
|||
|
|||
var cshtmlFiles = projectEngine.FileSystem.EnumerateItems(targetProjectDirectory) |
|||
.Where(x => File.ReadAllText(x.PhysicalPath).Contains("@inherits AbpCompilationRazorPageBase")) |
|||
.ToList(); |
|||
|
|||
if (!cshtmlFiles.Any()) |
|||
{ |
|||
Logger.LogInformation("No .cshtml or .razor files were found."); |
|||
return results; |
|||
} |
|||
|
|||
foreach (var item in cshtmlFiles) |
|||
{ |
|||
Logger.LogInformation(" Generating code file for page {0} ...", item.FileName); |
|||
|
|||
results.Add(GenerateCodeFile(projectEngine, item)); |
|||
|
|||
Logger.LogInformation(" Done!"); |
|||
} |
|||
|
|||
return results; |
|||
} |
|||
|
|||
private RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem) |
|||
{ |
|||
var projectItemWrapper = new FileSystemRazorProjectItemWrapper(Logger, projectItem); |
|||
var codeDocument = projectEngine.Process(projectItemWrapper); |
|||
var cSharpDocument = codeDocument.GetCSharpDocument(); |
|||
if (cSharpDocument.Diagnostics.Any()) |
|||
{ |
|||
var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics); |
|||
Logger.LogInformation($"One or more parse errors encountered. This will not prevent the generator from continuing: {Environment.NewLine}{diagnostics}."); |
|||
} |
|||
|
|||
var generatedCodeFilePath = Path.ChangeExtension(projectItem.PhysicalPath, ".Designer.cs"); |
|||
return new RazorPageGeneratorResult |
|||
{ |
|||
FilePath = generatedCodeFilePath, |
|||
GeneratedCode = cSharpDocument.GeneratedCode, |
|||
}; |
|||
} |
|||
|
|||
private class SuppressChecksumOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature |
|||
{ |
|||
public int Order { get; set; } |
|||
|
|||
public void Configure(RazorCodeGenerationOptionsBuilder options) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
|
|||
options.SuppressChecksum = true; |
|||
} |
|||
} |
|||
|
|||
private class SuppressMetadataAttributesFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature |
|||
{ |
|||
public int Order { get; set; } |
|||
|
|||
public void Configure(RazorCodeGenerationOptionsBuilder options) |
|||
{ |
|||
Check.NotNull(options, nameof(options)); |
|||
options.SuppressMetadataAttributes = true; |
|||
} |
|||
} |
|||
|
|||
private class FileSystemRazorProjectItemWrapper : RazorProjectItem |
|||
{ |
|||
private readonly ILogger<GenerateRazorPage> _logger; |
|||
private readonly RazorProjectItem _source; |
|||
|
|||
public FileSystemRazorProjectItemWrapper(ILogger<GenerateRazorPage> logger, RazorProjectItem item) |
|||
{ |
|||
_logger = logger; |
|||
_source = item; |
|||
|
|||
// Mask the full name since we don't want a developer's local file paths to be committed.
|
|||
PhysicalPath = $"{_source.FileName}"; |
|||
} |
|||
|
|||
public override string BasePath => _source.BasePath; |
|||
|
|||
public override string FilePath => _source.FilePath; |
|||
|
|||
public override string PhysicalPath { get; } |
|||
|
|||
public override bool Exists => _source.Exists; |
|||
|
|||
public override Stream Read() |
|||
{ |
|||
var processedContent = ProcessFileIncludes(); |
|||
return new MemoryStream(Encoding.UTF8.GetBytes(processedContent)); |
|||
} |
|||
|
|||
private string ProcessFileIncludes() |
|||
{ |
|||
var basePath = Path.GetDirectoryName(_source.PhysicalPath); |
|||
var cshtmlContent = File.ReadAllText(_source.PhysicalPath); |
|||
|
|||
var startMatch = "<%$ include: "; |
|||
var endMatch = " %>"; |
|||
var startIndex = 0; |
|||
while (startIndex < cshtmlContent.Length) |
|||
{ |
|||
startIndex = cshtmlContent.IndexOf(startMatch, startIndex, StringComparison.Ordinal); |
|||
if (startIndex == -1) |
|||
{ |
|||
break; |
|||
} |
|||
var endIndex = cshtmlContent.IndexOf(endMatch, startIndex, StringComparison.Ordinal); |
|||
if (endIndex == -1) |
|||
{ |
|||
throw new InvalidOperationException($"Invalid include file format in {_source.PhysicalPath}. Usage example: <%$ include: ErrorPage.js %>"); |
|||
} |
|||
var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length)); |
|||
_logger.LogInformation(" Inlining file {0}", includeFileName); |
|||
var includeFileContent = File.ReadAllText(Path.Combine(basePath, includeFileName)); |
|||
cshtmlContent = string.Concat(cshtmlContent.Substring(0, startIndex), includeFileContent, cshtmlContent.Substring(endIndex + endMatch.Length)); |
|||
startIndex += includeFileContent.Length; |
|||
} |
|||
return cshtmlContent; |
|||
} |
|||
} |
|||
|
|||
private class RazorPageGeneratorResult |
|||
{ |
|||
public string FilePath { get; set; } |
|||
|
|||
public string GeneratedCode { get; set; } |
|||
} |
|||
} |
|||