mirror of https://github.com/abpframework/abp.git
179 changed files with 3587 additions and 1173 deletions
@ -0,0 +1,132 @@ |
|||||
|
# ABP Framework v2.0 and the ABP Commercial |
||||
|
|
||||
|
ABP Framework v2.0 has been released this week. This post explains why we have released an **early major version** and what is changed with version 2.0. |
||||
|
|
||||
|
In addition to the v2.0 release, we have also announced **ABP Commercial** which is a set of professional modules, tools, themes, and services built on top of the open-source ABP framework. |
||||
|
|
||||
|
## ABP Framework v2.0 |
||||
|
|
||||
|
### Why 2.0 instead of 1.2? |
||||
|
|
||||
|
It was planned to release v1.2 after the [v1.1.2](https://github.com/abpframework/abp/releases/tag/1.1.2) release. However, [it is reported](https://github.com/abpframework/abp/issues/2026) that v1.x has some **performance** and **stability** issues on Linux, especially when you deploy your application to **Linux** containers with **low CPU and memory** resources. |
||||
|
|
||||
|
We have investigated the problem deeply and have seen that the root cause of the problem was related to the implementation of **intercepting `async` methods**. Besides, there were some **`async` over `sync`** usages that effected the thread pool optimization. |
||||
|
|
||||
|
Finally, we **solved all the problems** with the great help of the **community**. But we also had some important **design decisions** which cause some **breaking changes** and we had to change the major version number of the framework because of the **semantic versioning**. |
||||
|
|
||||
|
Most of the applications won't be affected by [the breaking changes](https://github.com/abpframework/abp/releases), or it will be trivial to make these necessary changes. |
||||
|
|
||||
|
### Breaking Changes |
||||
|
|
||||
|
#### Removed Some Sync APIs |
||||
|
|
||||
|
Some of the interceptors are required to use `async` APIs. When they intercept `sync` methods, they need to call `async` over `sync`. This eventually ends up with `async` over `sync` problem. That's why we have [removed some sync APIs](https://github.com/abpframework/abp/pull/2464). |
||||
|
|
||||
|
**`Async` over `sync`** pattern is a classical problem of `C#` when you need to **call an `async` method inside a `sync` method**. While there are some workarounds to this problem, they all have **disadvantages** and it is suggested to **not write** such code at all. You can find many documents related to this topic on the web. |
||||
|
|
||||
|
To avoid this problem, we have removed: |
||||
|
|
||||
|
- `sync` [repository](https://docs.abp.io/en/abp/latest/Repositories) methods (like `insert`, `update`, etc...), |
||||
|
- `sync` APIs of the [unit of work](https://docs.abp.io/en/abp/latest/Unit-Of-Work), |
||||
|
- `sync` APIs of the [background jobs](https://docs.abp.io/en/abp/latest/Background-Jobs), |
||||
|
- `sync` APIs of the [audit logging](https://docs.abp.io/en/abp/latest/Audit-Logging), |
||||
|
- some other rarely used `sync` APIs. |
||||
|
|
||||
|
If you get any compile error, just use the `async` versions of these APIs. |
||||
|
|
||||
|
#### Always Async! |
||||
|
|
||||
|
Beginning from the v2.0, the ABP framework assumes that you are writing your application code `async` first. Otherwise, some framework functionalities may not properly work. |
||||
|
|
||||
|
It is suggested to write `async` to all your [application services](https://docs.abp.io/en/abp/latest/Application-Services), [repository methods](https://docs.abp.io/en/abp/latest/Repositories), controller actions, page handlers. |
||||
|
|
||||
|
Even if your application service method doesn't need to be `async` , set it as `async` , because interceptors perform `async` operations (for authorization, unit of work, etc...). You can return `Task.Completed` from a method that doesn't make an `async` call. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````csharp |
||||
|
public Task<int> GetValueAsync() |
||||
|
{ |
||||
|
//this method doesn't make any async call. |
||||
|
return Task.CompletedTask(42); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
The example above normally doesn't need to be `async` because it doesn't perform an `async` call. However, making it `async` helps the ABP framework to run interceptors without `async` over sync calls. |
||||
|
|
||||
|
This rule doesn't force you to write every method `async` . This would not be good and would be tedious. It is only needed for the intercepted services (especially for [application services](https://docs.abp.io/en/abp/latest/Application-Services) and [repository methods](https://docs.abp.io/en/abp/latest/Repositories)) |
||||
|
|
||||
|
#### Other Breaking Changes |
||||
|
|
||||
|
See [the release notes](https://github.com/abpframework/abp/releases/tag/2.0.0) for the other breaking changes. Most of them will not affect your application code. |
||||
|
|
||||
|
### New Features |
||||
|
|
||||
|
This release also contains some new features and tens of enhancements: |
||||
|
|
||||
|
- [#2597](https://github.com/abpframework/abp/pull/2597) New `Volo.Abp.AspNetCore.Serilog` package. |
||||
|
- [#2526](https://github.com/abpframework/abp/issues/2526) Client-side validation for the dynamic `C#` client proxies. |
||||
|
- [#2374](https://github.com/abpframework/abp/issues/2374) `Async` background jobs. |
||||
|
- [#265](https://github.com/abpframework/abp/issues/265) Managing the application shutdown. |
||||
|
- [#2472](https://github.com/abpframework/abp/issues/2472) Implemented `DeviceFlowCodes` and `TokenCleanupService` for the `IdentityServer` module. |
||||
|
|
||||
|
See [the release notes](https://github.com/abpframework/abp/releases/tag/2.0.0) for the complete list of features, enhancements and bug fixes. |
||||
|
|
||||
|
### Documentation |
||||
|
|
||||
|
We have completed some missing documentation with the v2.0 release. In the following weeks, we will mostly focus on the basic documentation and tutorials. |
||||
|
|
||||
|
## ABP Commercial |
||||
|
|
||||
|
[ABP Commercial](https://commercial.abp.io/) is a set of professional **modules, tools, themes, and services** built on top of the open-source ABP framework. |
||||
|
|
||||
|
- It provides [professional modules](https://commercial.abp.io/modules) in addition to the ABP Framework's free & [open source modules](https://docs.abp.io/en/abp/latest/Modules/Index). |
||||
|
- It includes a beautiful a [UI theme](https://commercial.abp.io/themes). |
||||
|
- It provides [ABP Suite](https://commercial.abp.io/tools/suite); A tool to assist your development to make you more productive. It currently can create full-stack CRUD pages in a few seconds by configuring your entity properties. More functionalities will be added over time. |
||||
|
- [Premium support](https://commercial.abp.io/support) for enterprise companies. |
||||
|
|
||||
|
In addition to these standard set of features, we will provide customer basis services. See the [commercial.abp.io](https://commercial.abp.io/) web site for other details. |
||||
|
|
||||
|
### ABP Framework vs the ABP Commercial |
||||
|
|
||||
|
The ABP Commercial **is not a paid version** of the ABP Framework. You can consider it as **set of additional benefits** for professional companies. You can use it to save your time and develop your product faster. |
||||
|
|
||||
|
ABP Framework is open source & free and will always be free! |
||||
|
|
||||
|
As a principle, we build the main infrastructure as open-source and sell additional pre-built application features, themes, and tools. The main idea similar to the [ASP.NET Boilerplate](https://aspnetboilerplate.com/) & the [ASP.NET Zero](https://aspnetzero.com/) products. |
||||
|
|
||||
|
Buying a commercial license saves you significant time and effort and you can focus on your own business, besides you get dedicated and high priority support. Also, you will be supporting the ABP core team since we are spending most of our time to develop, maintain and support the open-source ABP Framework. |
||||
|
|
||||
|
### Pricing |
||||
|
|
||||
|
You can build **unlimited projects/products**, sell to **unlimited customers**, host **unlimited servers** without any restriction. Pricing is mostly based on the **developer count**, **support level** and **source code** requirement. There are three main packages; |
||||
|
|
||||
|
- **Team license**: Includes all the modules, themes and tools. Allows developing your product with up to 3 developers. You can buy additional developer licenses. |
||||
|
- **Business license**: Allows downloading the source code of all the modules and the themes. Also, it includes 5 developer licenses by default. You can buy additional developer licenses. |
||||
|
- **Enterprise license**: Provides unlimited and private support in addition to the benefits of the business license. |
||||
|
|
||||
|
See the [pricing page](https://commercial.abp.io/pricing) for details. In addition to the standard packages, we are also providing custom services and custom licensing. [Contact us](https://commercial.abp.io/contact) if you have further questions. |
||||
|
|
||||
|
#### License Comparison |
||||
|
|
||||
|
The license price changes based on your developer count, support level and source-code access. |
||||
|
|
||||
|
##### The Source-Code |
||||
|
|
||||
|
Team license doesn't include the source-code of the pre-built modules & themes. It uses all these modules as `NuGet` & `NPM` packages. In this way, you can easily get new features and bug fixes by just updating the package dependencies. But you can't access their source-code. So you don't have the possibility to embed a module's source code into your application and freely change the source-code. |
||||
|
|
||||
|
Pre-built modules provide some level of customization and extensibility and allow you to override services, UI parts and so on. We are working on to make them much more customizable and extensible. If you don't need to make major changes in the pre-built modules, the team license will be ideal for you, because it is cheaper and allows you to easily get new features and bug fixes. |
||||
|
|
||||
|
Business and Enterprise licenses allow you to download the source-code of any module or the theme when you need it. They also use the same startup template with the team license, so all modules are used as `NuGet` & `NPM` packages. But in case of need, you can remove the package dependencies for a module and embed its source-code into your own solution to completely customize it. In this case, upgrading the module will not be as easy as before when a new version is available. You don't have to upgrade it, surely! But if you want, you should do it yourself using some merge tool or `Git` branch system. |
||||
|
|
||||
|
#### License Lifetime |
||||
|
|
||||
|
ABP Commercial license is **perpetual**, which means you can **use it forever** and continue to develop your applications. |
||||
|
|
||||
|
However, the following services are covered for one year: |
||||
|
|
||||
|
- Premium **support** ends after one year. You can continue to get community support. |
||||
|
- You can not get **updates** of the modules & the themes after one year. You can continue to use the last obtained version. You can even get bug fixes and enhancements for your current major version. |
||||
|
- You can use the **ABP Suite** tool for one year. |
||||
|
|
||||
|
If you want to continue to get these benefits, you can extend your license period. Renewing price is 20% less than the regular price. |
||||
@ -0,0 +1,3 @@ |
|||||
|
# IdentityServer Module |
||||
|
|
||||
|
TODO |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Volo.Abp.AspNetCore.Serilog; |
||||
|
|
||||
|
namespace Microsoft.AspNetCore.Builder |
||||
|
{ |
||||
|
public static class AbpAspNetCoreSerilogApplicationBuilderExtensions |
||||
|
{ |
||||
|
public static IApplicationBuilder UseAbpSerilogEnrichers(this IApplicationBuilder app) |
||||
|
{ |
||||
|
return app |
||||
|
.UseMiddleware<AbpSerilogMiddleware>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
{ |
||||
|
"iisSettings": { |
||||
|
"windowsAuthentication": false, |
||||
|
"anonymousAuthentication": true, |
||||
|
"iisExpress": { |
||||
|
"applicationUrl": "http://localhost:61851/", |
||||
|
"sslPort": 44301 |
||||
|
} |
||||
|
}, |
||||
|
"profiles": { |
||||
|
"IIS Express": { |
||||
|
"commandName": "IISExpress", |
||||
|
"launchBrowser": true, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
} |
||||
|
}, |
||||
|
"Volo.Abp.AspNetCore.Serilog": { |
||||
|
"commandName": "Project", |
||||
|
"launchBrowser": true, |
||||
|
"environmentVariables": { |
||||
|
"ASPNETCORE_ENVIRONMENT": "Development" |
||||
|
}, |
||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk.Web"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
<AssemblyName>Volo.Abp.AspNetCore.Serilog</AssemblyName> |
||||
|
<PackageId>Volo.Abp.AspNetCore.Serilog</PackageId> |
||||
|
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<IsPackable>true</IsPackable> |
||||
|
<OutputType>Library</OutputType> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.AspNetCore\Volo.Abp.AspNetCore.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.MultiTenancy\Volo.Abp.MultiTenancy.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Serilog" Version="2.8.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Serilog |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpMultiTenancyModule), |
||||
|
typeof(AbpAspNetCoreModule) |
||||
|
)] |
||||
|
public class AbpAspNetCoreSerilogModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
namespace Volo.Abp.AspNetCore.Serilog |
||||
|
{ |
||||
|
public class AbpAspNetCoreSerilogOptions |
||||
|
{ |
||||
|
public AllEnricherPropertyNames EnricherPropertyNames { get; } = new AllEnricherPropertyNames(); |
||||
|
|
||||
|
public class AllEnricherPropertyNames |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Default value: "TenantId".
|
||||
|
/// </summary>
|
||||
|
public string TenantId { get; set; } = "TenantId"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Default value: "UserId".
|
||||
|
/// </summary>
|
||||
|
public string UserId { get; set; } = "UserId"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Default value: "ClientId".
|
||||
|
/// </summary>
|
||||
|
public string ClientId { get; set; } = "ClientId"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Default value: "CorrelationId".
|
||||
|
/// </summary>
|
||||
|
public string CorrelationId { get; set; } = "CorrelationId"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Serilog.Context; |
||||
|
using Serilog.Core; |
||||
|
using Serilog.Core.Enrichers; |
||||
|
using Volo.Abp.Clients; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Tracing; |
||||
|
using Volo.Abp.Users; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Serilog |
||||
|
{ |
||||
|
public class AbpSerilogMiddleware : IMiddleware, ITransientDependency |
||||
|
{ |
||||
|
private readonly ICurrentClient _currentClient; |
||||
|
private readonly ICurrentTenant _currentTenant; |
||||
|
private readonly ICurrentUser _currentUser; |
||||
|
private readonly ICorrelationIdProvider _correlationIdProvider; |
||||
|
private readonly AbpAspNetCoreSerilogOptions _options; |
||||
|
|
||||
|
public AbpSerilogMiddleware( |
||||
|
ICurrentTenant currentTenant, |
||||
|
ICurrentUser currentUser, |
||||
|
ICurrentClient currentClient, |
||||
|
ICorrelationIdProvider correlationIdProvider, |
||||
|
IOptions<AbpAspNetCoreSerilogOptions> options) |
||||
|
{ |
||||
|
_currentTenant = currentTenant; |
||||
|
_currentUser = currentUser; |
||||
|
_currentClient = currentClient; |
||||
|
_correlationIdProvider = correlationIdProvider; |
||||
|
_options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next) |
||||
|
{ |
||||
|
var enrichers = new List<ILogEventEnricher>(); |
||||
|
|
||||
|
if (_currentTenant?.Id != null) |
||||
|
{ |
||||
|
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.TenantId, _currentTenant.Id)); |
||||
|
} |
||||
|
|
||||
|
if (_currentUser?.Id != null) |
||||
|
{ |
||||
|
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.UserId, _currentUser.Id)); |
||||
|
} |
||||
|
|
||||
|
if (_currentClient?.Id != null) |
||||
|
{ |
||||
|
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.ClientId, _currentClient.Id)); |
||||
|
} |
||||
|
|
||||
|
var correlationId = _correlationIdProvider.Get(); |
||||
|
if (!string.IsNullOrEmpty(correlationId)) |
||||
|
{ |
||||
|
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.CorrelationId, correlationId)); |
||||
|
} |
||||
|
|
||||
|
using (LogContext.Push(enrichers.ToArray())) |
||||
|
{ |
||||
|
await next(context).ConfigureAwait(false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<Import Project="..\..\..\common.test.props" /> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.1</TargetFramework> |
||||
|
<AssemblyName>Volo.Abp.AspNetCore.Serilog.Tests</AssemblyName> |
||||
|
<PackageId>Volo.Abp.AspNetCore.Serilog.Tests</PackageId> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> |
||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.MultiTenancy\Volo.Abp.AspNetCore.MultiTenancy.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.AspNetCore.Tests\Volo.Abp.AspNetCore.Tests.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,39 @@ |
|||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Volo.Abp.AspNetCore.MultiTenancy; |
||||
|
using Volo.Abp.AspNetCore.Mvc; |
||||
|
using Volo.Abp.AspNetCore.Serilog; |
||||
|
using Volo.Abp.AspNetCore.TestBase; |
||||
|
using Volo.Abp.Autofac; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.App |
||||
|
{ |
||||
|
[DependsOn( |
||||
|
typeof(AbpAspNetCoreTestBaseModule), |
||||
|
typeof(AbpAspNetCoreMvcModule), |
||||
|
typeof(AbpAspNetCoreMultiTenancyModule), |
||||
|
typeof(AbpAspNetCoreSerilogModule), |
||||
|
typeof(AbpAutofacModule) |
||||
|
)] |
||||
|
public class AbpSerilogTestModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpMultiTenancyOptions>(options => { options.IsEnabled = true; }); |
||||
|
} |
||||
|
|
||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
||||
|
{ |
||||
|
var app = context.GetApplicationBuilder(); |
||||
|
|
||||
|
app.UseCorrelationId(); |
||||
|
app.UseRouting(); |
||||
|
app.UseAuthorization(); |
||||
|
app.UseMultiTenancy(); |
||||
|
app.UseAuditing(); |
||||
|
app.UseAbpSerilogEnrichers(); |
||||
|
app.UseMvcWithDefaultRouteAndArea(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Serilog.Core; |
||||
|
using Serilog.Events; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.App |
||||
|
{ |
||||
|
public class CollectingSink : ILogEventSink |
||||
|
{ |
||||
|
public List<LogEvent> Events { get; } = new List<LogEvent>(); |
||||
|
|
||||
|
public LogEvent SingleEvent => Events.Single(); |
||||
|
|
||||
|
public void Emit(LogEvent logEvent) |
||||
|
{ |
||||
|
Events.Add(logEvent); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Volo.Abp.AspNetCore.Mvc; |
||||
|
using Volo.Abp.Tracing; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.App |
||||
|
{ |
||||
|
public class SerilogTestController : AbpController |
||||
|
{ |
||||
|
private readonly ICorrelationIdProvider _correlationIdProvider; |
||||
|
|
||||
|
public SerilogTestController(ICorrelationIdProvider correlationIdProvider) |
||||
|
{ |
||||
|
_correlationIdProvider = correlationIdProvider; |
||||
|
} |
||||
|
|
||||
|
public ActionResult Index() |
||||
|
{ |
||||
|
return Content("Index-Result"); |
||||
|
} |
||||
|
|
||||
|
public ActionResult CorrelationId() |
||||
|
{ |
||||
|
return Content(_correlationIdProvider.Get()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.Hosting; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.App |
||||
|
{ |
||||
|
public class Startup |
||||
|
{ |
||||
|
public void ConfigureServices(IServiceCollection services) |
||||
|
{ |
||||
|
services.AddApplication<AbpSerilogTestModule>(); |
||||
|
} |
||||
|
|
||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILoggerFactory loggerFactory) |
||||
|
{ |
||||
|
app.InitializeApplication(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System.Linq; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Serilog; |
||||
|
using Serilog.Events; |
||||
|
using Volo.Abp.AspNetCore.App; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Serilog |
||||
|
{ |
||||
|
public class AbpSerilogTestBase : AbpAspNetCoreTestBase<App.Startup> |
||||
|
{ |
||||
|
protected readonly CollectingSink CollectingSink = new CollectingSink(); |
||||
|
|
||||
|
protected override IHostBuilder CreateHostBuilder() |
||||
|
{ |
||||
|
Log.Logger = new LoggerConfiguration() |
||||
|
.MinimumLevel.Information() |
||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) |
||||
|
.Enrich.FromLogContext() |
||||
|
.WriteTo.Sink(CollectingSink) |
||||
|
.CreateLogger(); |
||||
|
|
||||
|
return base.CreateHostBuilder() |
||||
|
.UseSerilog(); |
||||
|
} |
||||
|
|
||||
|
protected LogEvent GetLogEvent(string text) |
||||
|
{ |
||||
|
return CollectingSink.Events.FirstOrDefault(m => m.MessageTemplate.Text == text); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Serilog.Events; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.AspNetCore.App; |
||||
|
using Volo.Abp.AspNetCore.MultiTenancy; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.MultiTenancy.ConfigurationStore; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Serilog |
||||
|
{ |
||||
|
public class Serilog_Enrichers_Tests : AbpSerilogTestBase |
||||
|
{ |
||||
|
private const string ExecutedEndpointLogEventText = "Executed endpoint '{EndpointName}'"; |
||||
|
|
||||
|
private readonly Guid _testTenantId = Guid.NewGuid(); |
||||
|
private readonly string _testTenantName = "acme"; |
||||
|
|
||||
|
private readonly AbpAspNetCoreMultiTenancyOptions _tenancyOptions; |
||||
|
private readonly AbpAspNetCoreSerilogOptions _serilogOptions; |
||||
|
private readonly ILogger<Serilog_Enrichers_Tests> _logger; |
||||
|
|
||||
|
public Serilog_Enrichers_Tests() |
||||
|
{ |
||||
|
_tenancyOptions = ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreMultiTenancyOptions>>().Value; |
||||
|
_serilogOptions = |
||||
|
ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreSerilogOptions>>().Value; |
||||
|
_logger = ServiceProvider.GetRequiredService<ILogger<Serilog_Enrichers_Tests>>(); |
||||
|
} |
||||
|
|
||||
|
protected override IHostBuilder CreateHostBuilder() |
||||
|
{ |
||||
|
return base.CreateHostBuilder().ConfigureServices(services => |
||||
|
{ |
||||
|
services.Configure<AbpDefaultTenantStoreOptions>(options => |
||||
|
{ |
||||
|
options.Tenants = new[] |
||||
|
{ |
||||
|
new TenantConfiguration(_testTenantId, _testTenantName) |
||||
|
}; |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task TenantId_Not_Set_Test() |
||||
|
{ |
||||
|
var url = GetUrl<SerilogTestController>(nameof(SerilogTestController.Index)); |
||||
|
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false); |
||||
|
|
||||
|
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText); |
||||
|
|
||||
|
executedLogEvent.ShouldNotBeNull(); |
||||
|
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.TenantId) |
||||
|
.ShouldBe(false); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task TenantId_Set_Test() |
||||
|
{ |
||||
|
var url = |
||||
|
GetUrl<SerilogTestController>(nameof(SerilogTestController.Index)) + |
||||
|
$"?{_tenancyOptions.TenantKey}={_testTenantName}"; |
||||
|
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false); |
||||
|
|
||||
|
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText); |
||||
|
|
||||
|
executedLogEvent.ShouldNotBeNull(); |
||||
|
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.TenantId) |
||||
|
.ShouldBe(true); |
||||
|
((ScalarValue) executedLogEvent.Properties[_serilogOptions.EnricherPropertyNames.TenantId]).Value |
||||
|
.ShouldBe(_testTenantId); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task CorrelationId_Enrichers_Test() |
||||
|
{ |
||||
|
var url = GetUrl<SerilogTestController>(nameof(SerilogTestController.CorrelationId)); |
||||
|
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false); |
||||
|
|
||||
|
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText); |
||||
|
|
||||
|
executedLogEvent.ShouldNotBeNull(); |
||||
|
|
||||
|
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.CorrelationId) |
||||
|
.ShouldNotBeNull(); |
||||
|
|
||||
|
((ScalarValue) executedLogEvent.Properties[_serilogOptions.EnricherPropertyNames.CorrelationId]).Value |
||||
|
.ShouldBe(result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,10 @@ |
|||||
# Blogging Module |
# Blogging Module |
||||
|
|
||||
This module is used for ABP blog: https://abp.io/blog/abp/ |
This module adds a simple blog to your ABP application; |
||||
|
|
||||
|
* Allows to create multiple blogs in a single application. |
||||
|
* Supports the Markdown format. |
||||
|
* Allows to write comment for a post. |
||||
|
* Allows to assign tags to the blog posts. |
||||
|
|
||||
|
See the [blog.abp.io](https://blog.abp.io/) website as a live example of the blogging module. |
||||
@ -1,13 +1,15 @@ |
|||||
# Docs Module |
# Docs Module |
||||
This module is used to create technical documentation web sites. [abp.io](https://abp.io) web site uses this module for its documentation. |
This module is used to create technical documentation web sites; |
||||
|
|
||||
### Screenshot |
* Built-in **GitHub integration**: Directly write and manage documents on GitHub. |
||||
|
* **Versioning** support directly integrated to GitHub releases. |
||||
|
* Supports **multi-language** (with fallback support to the default language). |
||||
|
* Supports the **Markdown** and HTML formats. |
||||
|
* Provides a **navigation** and an **outline** section. |
||||
|
* Allows to host **multiple projects** documentation in a single application. |
||||
|
* Links to the file on GitHub, so anyone can easily contribute by clicking to the **Edit link**. |
||||
|
* In addition to the GitHub source, allows to simply use a folder as the documentation source. |
||||
|
|
||||
 |
ABP Framework uses this module to host its documentation. See [docs.abp.io](https://docs.abp.io/). A screenshot from the ABP documentation: |
||||
|
|
||||
### Main Features |
 |
||||
|
|
||||
* Retrieves documents from a Github repository. |
|
||||
* Supports Markdown document formatting. |
|
||||
* Supports versioning (integrated to Github releases). |
|
||||
* Supports multiple projects. |
|
||||
|
|||||
|
Before Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 260 KiB |
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.Abp.Identity |
||||
|
{ |
||||
|
public class IdentityRoleNameChangedEvent |
||||
|
{ |
||||
|
public IdentityRole IdentityRole { get; set; } |
||||
|
public string OldName { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Authorization.Permissions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Entities.Events; |
||||
|
using Volo.Abp.EventBus; |
||||
|
using Volo.Abp.Identity; |
||||
|
|
||||
|
namespace Volo.Abp.PermissionManagement.Identity |
||||
|
{ |
||||
|
public class RoleUpdateEventHandler : |
||||
|
ILocalEventHandler<IdentityRoleNameChangedEvent>, |
||||
|
ITransientDependency |
||||
|
{ |
||||
|
protected IIdentityRoleRepository RoleRepository { get; } |
||||
|
protected IPermissionManager PermissionManager { get; } |
||||
|
protected IPermissionGrantRepository PermissionGrantRepository { get; } |
||||
|
|
||||
|
public RoleUpdateEventHandler( |
||||
|
IIdentityRoleRepository roleRepository, |
||||
|
IPermissionManager permissionManager, |
||||
|
IPermissionGrantRepository permissionGrantRepository) |
||||
|
{ |
||||
|
RoleRepository = roleRepository; |
||||
|
PermissionManager = permissionManager; |
||||
|
PermissionGrantRepository = permissionGrantRepository; |
||||
|
} |
||||
|
|
||||
|
public async Task HandleEventAsync(IdentityRoleNameChangedEvent eventData) |
||||
|
{ |
||||
|
var role = await RoleRepository.FindAsync(eventData.IdentityRole.Id, false).ConfigureAwait(false); |
||||
|
if (role == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync(RolePermissionValueProvider.ProviderName, eventData.OldName).ConfigureAwait(false); |
||||
|
foreach (var permissionGrant in permissionGrantsInRole) |
||||
|
{ |
||||
|
await PermissionManager.UpdateProviderKeyAsync(permissionGrant, eventData.IdentityRole.Name).ConfigureAwait(false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Shouldly; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.EventBus.Distributed; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.PermissionManagement; |
||||
|
using Volo.Abp.Uow; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Identity |
||||
|
{ |
||||
|
public class RoleChangingEvents_Test : AbpIdentityDomainTestBase |
||||
|
{ |
||||
|
protected readonly IIdentityRoleRepository RoleRepository; |
||||
|
protected readonly IPermissionGrantRepository PermissionGrantRepository; |
||||
|
protected readonly IdentityRoleManager RoleManager; |
||||
|
protected readonly ILookupNormalizer LookupNormalizer; |
||||
|
protected readonly IGuidGenerator GuidGenerator; |
||||
|
protected readonly IUnitOfWorkManager UowManager; |
||||
|
|
||||
|
public RoleChangingEvents_Test() |
||||
|
{ |
||||
|
RoleRepository = GetRequiredService<IIdentityRoleRepository>(); ; |
||||
|
PermissionGrantRepository = GetRequiredService<IPermissionGrantRepository>(); ; |
||||
|
RoleManager = GetRequiredService<IdentityRoleManager>(); ; |
||||
|
LookupNormalizer = GetRequiredService<ILookupNormalizer>(); ; |
||||
|
GuidGenerator = GetRequiredService<IGuidGenerator>(); |
||||
|
UowManager = GetRequiredService<IUnitOfWorkManager>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Role_Update_Event_Test() |
||||
|
{ |
||||
|
var role = await RoleRepository |
||||
|
.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName("moderator")) |
||||
|
.ConfigureAwait(false); |
||||
|
|
||||
|
var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync("R", role.Name).ConfigureAwait(false); |
||||
|
permissionGrantsInRole.ShouldNotBeNull(); |
||||
|
permissionGrantsInRole.Count.ShouldBeGreaterThan(0); |
||||
|
var count = permissionGrantsInRole.Count; |
||||
|
|
||||
|
using (var uow = UowManager.Begin()) |
||||
|
{ |
||||
|
var identityResult = await RoleManager.SetRoleNameAsync(role, "TestModerator").ConfigureAwait(false); |
||||
|
identityResult.Succeeded.ShouldBeTrue(); |
||||
|
var xx = await RoleRepository.UpdateAsync(role).ConfigureAwait(false); |
||||
|
await uow.CompleteAsync().ConfigureAwait(false); |
||||
|
} |
||||
|
|
||||
|
role = await RoleRepository.GetAsync(role.Id).ConfigureAwait(false); |
||||
|
role.Name.ShouldBe("TestModerator"); |
||||
|
|
||||
|
permissionGrantsInRole = await PermissionGrantRepository.GetListAsync("R", role.Name).ConfigureAwait(false); |
||||
|
permissionGrantsInRole.Count.ShouldBe(count); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 101 KiB |
@ -1,5 +1,7 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"packages": ["packages/*"], |
"packages": [ |
||||
|
"packages/*" |
||||
|
], |
||||
"npmClient": "yarn" |
"npmClient": "yarn" |
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
@ -1,12 +1,12 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/anchor-js", |
"name": "@abp/anchor-js", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/core": "^1.1.1", |
"@abp/core": "^2.0.0", |
||||
"anchor-js": "^4.2.2" |
"anchor-js": "^4.2.2" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,11 +1,11 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/aspnetcore.mvc.ui.theme.basic", |
"name": "@abp/aspnetcore.mvc.ui.theme.basic", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/aspnetcore.mvc.ui.theme.shared": "^1.1.1" |
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.0.0" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,24 +1,24 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/aspnetcore.mvc.ui.theme.shared", |
"name": "@abp/aspnetcore.mvc.ui.theme.shared", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/aspnetcore.mvc.ui": "^1.1.1", |
"@abp/aspnetcore.mvc.ui": "^2.0.0", |
||||
"@abp/bootstrap": "^1.1.1", |
"@abp/bootstrap": "^2.0.0", |
||||
"@abp/bootstrap-datepicker": "^1.1.1", |
"@abp/bootstrap-datepicker": "^2.0.0", |
||||
"@abp/datatables.net-bs4": "^1.1.1", |
"@abp/datatables.net-bs4": "^2.0.0", |
||||
"@abp/font-awesome": "^1.1.1", |
"@abp/font-awesome": "^2.0.0", |
||||
"@abp/jquery-form": "^1.1.1", |
"@abp/jquery-form": "^2.0.0", |
||||
"@abp/jquery-validation-unobtrusive": "^1.1.1", |
"@abp/jquery-validation-unobtrusive": "^2.0.0", |
||||
"@abp/lodash": "^1.1.1", |
"@abp/lodash": "^2.0.0", |
||||
"@abp/luxon": "^1.1.1", |
"@abp/luxon": "^2.0.0", |
||||
"@abp/malihu-custom-scrollbar-plugin": "^1.1.1", |
"@abp/malihu-custom-scrollbar-plugin": "^2.0.0", |
||||
"@abp/select2": "^1.1.1", |
"@abp/select2": "^2.0.0", |
||||
"@abp/sweetalert": "^1.1.1", |
"@abp/sweetalert": "^2.0.0", |
||||
"@abp/timeago": "^1.1.1", |
"@abp/timeago": "^2.0.0", |
||||
"@abp/toastr": "^1.1.1" |
"@abp/toastr": "^2.0.0" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,13 +1,13 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/blogging", |
"name": "@abp/blogging", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/aspnetcore.mvc.ui.theme.shared": "^1.1.1", |
"@abp/aspnetcore.mvc.ui.theme.shared": "^2.0.0", |
||||
"@abp/owl.carousel": "^1.1.1", |
"@abp/owl.carousel": "^2.0.0", |
||||
"@abp/tui-editor": "^1.1.1" |
"@abp/tui-editor": "^2.0.0" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/bootstrap", |
"name": "@abp/bootstrap", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/core": "^1.1.1", |
"@abp/core": "^2.0.0", |
||||
"bootstrap": "^4.3.1" |
"bootstrap": "^4.3.1" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/clipboard", |
"name": "@abp/clipboard", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/core": "^1.1.1", |
"@abp/core": "^2.0.0", |
||||
"clipboard": "^2.0.4" |
"clipboard": "^2.0.4" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/codemirror", |
"name": "@abp/codemirror", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/core": "^1.1.1", |
"@abp/core": "^2.0.0", |
||||
"codemirror": "^5.49.2" |
"codemirror": "^5.49.2" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,8 +1,8 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/core", |
"name": "@abp/core", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
@ -1,12 +1,12 @@ |
|||||
{ |
{ |
||||
"version": "1.1.1", |
"version": "2.0.0", |
||||
"name": "@abp/datatables.net-bs4", |
"name": "@abp/datatables.net-bs4", |
||||
"publishConfig": { |
"publishConfig": { |
||||
"access": "public" |
"access": "public" |
||||
}, |
}, |
||||
"dependencies": { |
"dependencies": { |
||||
"@abp/datatables.net": "^1.1.1", |
"@abp/datatables.net": "^2.0.0", |
||||
"datatables.net-bs4": "^1.10.20" |
"datatables.net-bs4": "^1.10.20" |
||||
}, |
}, |
||||
"gitHead": "b926d7a3fed6365550493ec00bdfe9da9dd6c541" |
"gitHead": "407ad749bb735eb9b6cc1ef6dd310253b9680883" |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue