Browse Source

Merge pull request #11 from abpframework/master

merge
pull/847/head
Marcelo Mohr Maciel 7 years ago
committed by GitHub
parent
commit
0c32628e3b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .dockerignore
  2. 45
      abp_io/src/Volo.AbpWebSite.Web/AbpWebSiteWebModule.cs
  3. 28
      abp_io/src/Volo.AbpWebSite.Web/CorrelationIdLogEventEnricher.cs
  4. 31
      abp_io/src/Volo.AbpWebSite.Web/Program.cs
  5. 10
      abp_io/src/Volo.AbpWebSite.Web/Startup.cs
  6. 2
      abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj
  7. 50
      build/build.ps1
  8. 2
      common.props
  9. 28
      docker/docker-compose.yml
  10. 1
      docker/down.ps1
  11. 18
      docker/haproxy.cfg
  12. 8
      docker/up.ps1
  13. 3
      docs/en/Audit-Logging.md
  14. 54
      docs/en/Blog-Posts/2019-02-22/Post.md
  15. BIN
      docs/en/Blog-Posts/2019-02-22/scott-and-jon.png
  16. 21
      docs/en/Getting-Started-AspNetCore-Application.md
  17. 1408
      docs/en/Samples/Microservice-Demo.md
  18. BIN
      docs/en/images/microservice-sample-authserver-home.png
  19. BIN
      docs/en/images/microservice-sample-authserver-login.png
  20. BIN
      docs/en/images/microservice-sample-backend-ui-permissions.png
  21. BIN
      docs/en/images/microservice-sample-backend-ui.png
  22. BIN
      docs/en/images/microservice-sample-blogservice-permission-in-database.png
  23. BIN
      docs/en/images/microservice-sample-kibana-1.png
  24. BIN
      docs/en/images/microservice-sample-kibana-2.png
  25. BIN
      docs/en/images/microservice-sample-product-module-in-solution.png
  26. BIN
      docs/en/images/microservice-sample-public-product-list.png
  27. BIN
      docs/en/images/microservice-sample-solution.png
  28. 3
      docs/zh-Hans/Domain-Services.md
  29. 3
      docs/zh-Hans/Dynamic-Proxying-Interceptors.md
  30. 3
      docs/zh-Hans/Guid-Generation.md
  31. 3
      docs/zh-Hans/Unit-Of-Work.md
  32. 3
      docs/zh-Hans/Validation.md
  33. 11
      docs/zh-Hans/docs-nav.json
  34. 25
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperAttributeExtensions.cs
  35. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
  36. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
  37. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
  38. 6
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs
  39. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs
  40. 34
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs
  41. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml
  42. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs
  43. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs
  44. 12
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Bundling/BasicThemeGlobalScriptContributor.cs
  45. 47
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml
  46. 36
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml
  47. 24
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css
  48. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.js
  49. 20
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs
  50. 2
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs
  51. 19
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs
  52. 2
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
  53. 11
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
  54. 15
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs
  55. 10
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
  56. 43
      framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs
  57. 9
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
  58. 2
      framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs
  59. 7
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs
  60. 11
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs
  61. 2
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs
  62. 2
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs
  63. 3
      framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs
  64. 13
      framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs
  65. 9
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs
  66. 9
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ExtraProperties_Tests.cs
  67. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs
  68. 53
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs
  69. 9
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs
  70. 113
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml
  71. 240
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs
  72. 4
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj
  73. 1
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml
  74. 7
      modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs
  75. 9
      modules/blogging/app/Volo.BloggingTestApp/Startup.cs
  76. 1
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs
  77. 2
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs
  78. 14
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs
  79. 13
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs
  80. 7
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs
  81. 10
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs
  82. 1
      modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj
  83. 11
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs
  84. 5
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs
  85. 5
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs
  86. 13
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs
  87. 65
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs
  88. 20
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs
  89. 0
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs
  90. 41
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs
  91. 7
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs
  92. 3
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs
  93. 46
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs
  94. 1
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs
  95. 12
      modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs
  96. 8
      modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs
  97. 4
      modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj
  98. 12
      modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs
  99. 36
      modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs
  100. 30
      modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs

3
samples/MicroserviceDemo/.dockerignore → .dockerignore

@ -10,4 +10,5 @@ docker-compose*
*/obj
README.md
LICENSE
.vscode
.vscode
.vs

45
abp_io/src/Volo.AbpWebSite.Web/AbpWebSiteWebModule.cs

@ -29,6 +29,7 @@ using Volo.Abp.UI;
using Volo.Abp.VirtualFileSystem;
using Volo.AbpWebSite.Bundling;
using Volo.Blogging;
using Volo.Blogging.Files;
using Volo.Docs;
namespace Volo.AbpWebSite
@ -55,24 +56,34 @@ namespace Volo.AbpWebSite
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
ConfigureLanguages(context.Services);
ConfigureDatabaseServices(context.Services, configuration);
ConfigureVirtualFileSystem(context.Services, hostingEnvironment);
ConfigureBundles(context.Services);
ConfigureTheme(context.Services);
ConfigureLanguages();
ConfigureDatabaseServices(configuration);
ConfigureVirtualFileSystem(hostingEnvironment);
ConfigureBundles();
ConfigureTheme();
ConfigureBlogging(hostingEnvironment);
}
private static void ConfigureLanguages(IServiceCollection services)
private void ConfigureBlogging(IHostingEnvironment hostingEnvironment)
{
services.Configure<AbpLocalizationOptions>(options =>
Configure<BlogFileOptions>(options =>
{
options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files");
options.FileUploadUrlRoot = "/files/";
});
}
private void ConfigureLanguages()
{
Configure<AbpLocalizationOptions>(options =>
{
options.Languages.Add(new LanguageInfo("en-US", "en-US", "English"));
});
}
private static void ConfigureBundles(IServiceCollection services)
private void ConfigureBundles()
{
services.Configure<BundlingOptions>(options =>
Configure<BundlingOptions>(options =>
{
options
.StyleBundles
@ -95,24 +106,24 @@ namespace Volo.AbpWebSite
});
}
private static void ConfigureDatabaseServices(IServiceCollection services, IConfigurationRoot configuration)
private void ConfigureDatabaseServices(IConfigurationRoot configuration)
{
services.Configure<DbConnectionOptions>(options =>
Configure<DbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = configuration.GetConnectionString("Default");
});
services.Configure<AbpDbContextOptions>(options =>
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer();
});
}
private static void ConfigureVirtualFileSystem(IServiceCollection services, IHostingEnvironment hostingEnvironment)
private void ConfigureVirtualFileSystem(IHostingEnvironment hostingEnvironment)
{
if (hostingEnvironment.IsDevelopment())
{
services.Configure<VirtualFileSystemOptions>(options =>
Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.ReplaceEmbeddedByPhysical<AbpUiModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}..{0}framework{0}src{0}Volo.Abp.UI", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<AbpAspNetCoreMvcUiModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}..{0}framework{0}src{0}Volo.Abp.AspNetCore.Mvc.UI", Path.DirectorySeparatorChar)));
@ -126,9 +137,9 @@ namespace Volo.AbpWebSite
}
}
private void ConfigureTheme(IServiceCollection services)
private void ConfigureTheme()
{
services.Configure<ThemingOptions>(options =>
Configure<ThemingOptions>(options =>
{
options.Themes.Add<AbpIoTheme>();
options.DefaultThemeName = AbpIoTheme.Name;
@ -140,6 +151,8 @@ namespace Volo.AbpWebSite
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
app.UseCorrelationId();
app.UseAbpRequestLocalization();
if (env.IsDevelopment())

28
abp_io/src/Volo.AbpWebSite.Web/CorrelationIdLogEventEnricher.cs

@ -0,0 +1,28 @@
using Serilog.Core;
using Serilog.Events;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Tracing;
namespace Volo.AbpWebSite
{
//This is for trial for now
public class CorrelationIdLogEventEnricher : ILogEventEnricher, ITransientDependency
{
private readonly ICorrelationIdProvider _correlationIdProvider;
public CorrelationIdLogEventEnricher(ICorrelationIdProvider correlationIdProvider)
{
_correlationIdProvider = correlationIdProvider;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddOrUpdateProperty(
new LogEventProperty(
"CorrelationId",
new ScalarValue("CorrId:" + _correlationIdProvider.Get())
)
);
}
}
}

31
abp_io/src/Volo.AbpWebSite.Web/Program.cs

@ -1,13 +1,37 @@
using System.IO;
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Serilog;
using Serilog.Events;
namespace Volo.AbpWebSite
{
public class Program
{
public static void Main(string[] args)
public static int Main(string[] args)
{
BuildWebHostInternal(args).Run();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() //TODO: Should be configurable!
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.File("Logs/logs.txt")
.CreateLogger();
try
{
Log.Information("Starting web host.");
BuildWebHostInternal(args).Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly!");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
internal static IWebHost BuildWebHostInternal(string[] args) =>
@ -16,6 +40,7 @@ namespace Volo.AbpWebSite
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseSerilog()
.Build();
}
}

10
abp_io/src/Volo.AbpWebSite.Web/Startup.cs

@ -3,7 +3,6 @@ using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Volo.Abp;
namespace Volo.AbpWebSite
@ -23,15 +22,6 @@ namespace Volo.AbpWebSite
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole()
.AddDebug()
.AddSerilog(new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File("Logs/logs.txt")
.CreateLogger()
);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
app.InitializeApplication();

2
abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj

@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.2.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
</ItemGroup>

50
build/build.ps1

@ -1,50 +0,0 @@
# COMMON PATHS
$buildFolder = (Get-Item -Path "./" -Verbose).FullName
$slnFolder = Join-Path $buildFolder "../"
$outputFolder = Join-Path $buildFolder "outputs"
$abpDeskFolder = Join-Path $slnFolder "src/AbpDesk"
$abpDeskWebFolder = Join-Path $abpDeskFolder "AbpDesk.Web.Mvc"
## CLEAR ######################################################################
Remove-Item $outputFolder -Force -Recurse
New-Item -Path $outputFolder -ItemType Directory
## RESTORE NUGET PACKAGES #####################################################
Set-Location $slnFolder
dotnet restore
## PUBLISH ASPDESK WEB ########################################################
Set-Location $abpDeskWebFolder
dotnet publish --output (Join-Path $outputFolder "AbpDesk/Web")
New-Item -Path (Join-Path $outputFolder "AbpDesk/Web/PlugIns") -ItemType Directory
Copy-Item (Join-Path $abpDeskFolder "Web_PlugIns/*") (Join-Path $outputFolder "AbpDesk/Web/PlugIns/")
## PUBLISH IDENTITY HTTP API HOST #############################################
Set-Location (Join-Path $slnFolder "src/Volo.Abp.Identity.HttpApi.Host")
dotnet publish --output (Join-Path $outputFolder "AbpIdentity/HttpApiHost")
## CREATE DOCKER IMAGES #######################################################
Set-Location (Join-Path $outputFolder "AbpDesk/Web")
docker rmi abpdesk/web -f
docker build -t abpdesk/web .
Set-Location (Join-Path $outputFolder "AbpIdentity/HttpApiHost")
docker rmi abpidentity/httpapihost -f
docker build -t abpidentity/httpapihost .
## DOCKER COMPOSE FILES #######################################################
Copy-Item (Join-Path $slnFolder "docker/*.*") $outputFolder
## FINALIZE ###################################################################
Set-Location $outputFolder

2
common.props

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>0.13.0</Version>
<Version>0.14.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io</PackageProjectUrl>

28
docker/docker-compose.yml

@ -1,28 +0,0 @@
version: '2'
services:
mongodb:
image: tutum/mongodb
environment:
- AUTH=no
ports:
- "27017:27017"
- "28017:28017"
abpidentity_httpapihost:
image: abpidentity/httpapihost
environment:
- ASPNETCORE_ENVIRONMENT=Staging
abpdesk_web:
image: abpdesk/web
environment:
- ASPNETCORE_ENVIRONMENT=Staging
load_balancer:
image: haproxy:1.7.1
volumes:
- "./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg"
ports:
- "9005:8080"

1
docker/down.ps1

@ -1 +0,0 @@
docker-compose down -v --rmi local

18
docker/haproxy.cfg

@ -1,18 +0,0 @@
global
maxconn 4096
defaults
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
listen http-in
bind *:8080
server web-1 outputs_abpdesk_web_1:80
server web-2 outputs_abpdesk_web_2:80
stats enable
stats uri /haproxy
stats refresh 1s

8
docker/up.ps1

@ -1,8 +0,0 @@
docker rm $(docker ps -aq)
docker-compose up -d mongodb
docker-compose up -d abpidentity_httpapihost
docker-compose up -d abpdesk_web
sleep 2
docker-compose scale abpdesk_web=2
sleep 2
docker-compose up -d load_balancer

3
docs/en/Audit-Logging.md

@ -0,0 +1,3 @@
# Audit Logging
TODO

54
docs/en/Blog-Posts/2019-02-22/Post.md

@ -0,0 +1,54 @@
# Microservice Demo, Projects Status and Road Map
After [the first announcement](https://abp.io/blog/abp/Abp-vNext-Announcement) on the ABP vNext, we have a lot of improvements on the codebase (1100+ commits on the [GitHub repository](https://github.com/abpframework/abp)). We've created features, samples, documentation and much more. In this post, I want to inform you about some news and the status of the project.
## Microservice Demo Solution
One of the major goals of the ABP framework is to provide a [convenient infrastructure to create microservice solutions](https://abp.io/documents/abp/latest/Microservice-Architecture).
We've been working to develop a microservice solution demo. Initial version was completed and [documented](https://abp.io/documents/abp/latest/Samples/Microservice-Demo). This sample solution aims to demonstrate a simple yet complete microservice solution;
- Has multiple, independent, self-deployable **microservices**.
- Multiple **web applications**, each uses a different API gateway.
- Has multiple **gateways** / BFFs (Backend for Frontends) developed using the [Ocelot](https://github.com/ThreeMammals/Ocelot) library.
- Has an **authentication service** developed using the [IdentityServer](https://identityserver.io/) framework. It's also a SSO (Single Sign On) application with necessary UIs.
- Has **multiple databases**. Some microservices has their own database while some services/applications shares a database (to demonstrate different use cases).
- Has different types of databases: **SQL Server** (with **Entity Framework Core** ORM) and **MongoDB**.
- Has a **console application** to show the simplest way of using a service by authenticating.
- Uses [Redis](https://redis.io/) for **distributed caching**.
- Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**.
- Uses [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications.
- Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)).
See [its documentation](https://abp.io/documents/abp/latest/Samples/Microservice-Demo) for a detailed explanation of the solution.
## Improvements/Features
We've worked on so many features including **distributed event bus** (with RabbitMQ integration), **IdentityServer4 integration** and enhancements for almost all features. We are continuously refactoring and adding tests to make the framework more stable and production ready. It is [rapidly growing](https://github.com/abpframework/abp/graphs/contributors).
## Road Map
There are still too much work to be done before the first stable release (v1.0). You can see [prioritized backlog items](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3ABacklog) on the GitHub repo.
According to our estimation, we have planned to release v1.0 in Q2 of 2019 (probably in May or June). So, not too much time to wait. We are also very excited for the first stable release.
We will also work on [the documentation](https://abp.io/documents/abp/latest) since it is far from complete now.
First release may not include a SPA template. However, we want to prepare a simple one if it can be possible. Haven't decided yet about the SPA framework. Alternatives: **Angular, React and Blazor**. Please write your thought as a comment to this post.
## Chinese Web Site
There is a big ABP community in China. They have created a Chinese version of the abp.io web site: https://cn.abp.io/ They are keeping it up to date. Thanks to the Chinese developers and especially to [Liming Ma](https://github.com/maliming).
## NDC {London} 2019
It was a pleasure to be in [NDC {London}](https://ndc-london.com/) 2019 as a partner. We've talked to many developers about the current ASP.NET Boilerplate and the ABP vNext and we got good feedbacks.
We also had a chance to talk with [Scott Hanselman](https://twitter.com/shanselman) and [Jon Galloway](https://twitter.com/jongalloway). They visited our booth and we talked about the ideas for ABP vNext. They liked features, approaches and the goal of new ABP framework. See some photos and comments on twitter:
![scott-and-jon](scott-and-jon.png)
## Follow It
* You can star and follow the **GitHub** repository: https://github.com/abpframework/abp
* You can follow the official **Twitter** account for news: https://twitter.com/abpframework

BIN
docs/en/Blog-Posts/2019-02-22/scott-and-jon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

21
docs/en/Getting-Started-AspNetCore-Application.md

@ -152,6 +152,27 @@ services.AddApplication<AppModule>(options =>
});
````
4. Update `Program.cs` to not use the `WebHost.CreateDefaultBuilder()` method since it uses the default DI container:
````csharp
public class Program
{
public static void Main(string[] args)
{
BuildWebHostInternal(args).Run();
}
public static IWebHost BuildWebHostInternal(string[] args) =>
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
}
````
## Source Code
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication).

1408
docs/en/Samples/Microservice-Demo.md

File diff suppressed because it is too large

BIN
docs/en/images/microservice-sample-authserver-home.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/images/microservice-sample-authserver-login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/en/images/microservice-sample-backend-ui-permissions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/en/images/microservice-sample-backend-ui.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
docs/en/images/microservice-sample-blogservice-permission-in-database.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
docs/en/images/microservice-sample-kibana-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/en/images/microservice-sample-kibana-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
docs/en/images/microservice-sample-product-module-in-solution.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/en/images/microservice-sample-public-product-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/en/images/microservice-sample-solution.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

3
docs/zh-Hans/Domain-Services.md

@ -0,0 +1,3 @@
# ABP Documentation
待添加

3
docs/zh-Hans/Dynamic-Proxying-Interceptors.md

@ -0,0 +1,3 @@
## Dynamic Proxying / Interceptors
待添加

3
docs/zh-Hans/Guid-Generation.md

@ -0,0 +1,3 @@
## Guid 生成
待添加

3
docs/zh-Hans/Unit-Of-Work.md

@ -0,0 +1,3 @@
## Unit of Work
待添加

3
docs/zh-Hans/Validation.md

@ -0,0 +1,3 @@
## Validation
待添加

11
docs/zh-Hans/docs-nav.json

@ -67,7 +67,8 @@
"path": "Exception-Handling.md"
},
{
"text": "验证"
"text": "验证",
"path": "Validation.md"
},
{
"text": "授权"
@ -160,7 +161,8 @@
"path": "Repositories.md"
},
{
"text": "领域服务"
"text": "领域服务",
"path": "Domain-Services.md"
},
{
"text": "规约"
@ -178,7 +180,8 @@
"text": "数据传输对象(DTO)"
},
{
"text": "工作单元"
"text": "工作单元",
"path": "Unit-Of-Work.md"
}
]
}
@ -189,7 +192,7 @@
"items": [
{
"text": "API",
"items": [
"items": [
{
"text": "自动API控制器",
"path": "AspNetCore/Auto-API-Controllers.md"

25
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperAttributeExtensions.cs

@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions
{
public static class TagHelperAttributeExtensions
{
public static string ToHtmlAttributeAsString(this TagHelperAttribute attribute)
{
return attribute.Name + "=\"" + attribute.Value + "\"";
}
public static string ToHtmlAttributesAsString(this List<TagHelperAttribute> attributes)
{
var attributesAsString = "";
foreach (var attribute in attributes)
{
attributesAsString += attribute.ToHtmlAttributeAsString() + " ";
}
return attributesAsString;
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs

@ -392,7 +392,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress)
{
var list = context.GetValue<List<FormGroupItem>>(FormGroupContents) ?? new List<FormGroupItem>();
surpress = list != null;
surpress = list == null;
if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\"")))
{

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs

@ -160,7 +160,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress)
{
var list = context.GetValue<List<FormGroupItem>>(FormGroupContents) ?? new List<FormGroupItem>();
surpress = list != null;
surpress = list == null;
if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\"")))
{

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs

@ -308,7 +308,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress)
{
var list = context.GetValue<List<FormGroupItem>>(FormGroupContents) ?? new List<FormGroupItem>();
surpress = list != null;
surpress = list == null;
if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\"")))
{

6
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs

@ -1,5 +1,9 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid
{
[HtmlTargetElement("abp-row")]
[HtmlTargetElement("abp-form-row")]
public class AbpRowTagHelper : AbpTagHelper<AbpRowTagHelper, AbpRowTagHelperService>
{
public VerticalAlign VAlign { get; set; } = VerticalAlign.Default;

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs

@ -7,8 +7,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (output.TagName == "abp-row")
{
output.Attributes.AddClass("row");
}
if (output.TagName == "abp-form-row")
{
output.Attributes.AddClass("form-row");
}
output.TagName = "div";
output.Attributes.AddClass("row");
ProcessVerticalAlign(output);
ProcessHorizontalAlign(output);

34
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions;
@ -13,7 +14,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab
var innerContent = await output.GetChildContentAsync();
var tabHeader = GetTabHeaderItem(context, output);
var tabContent = GetTabContentItem(innerContent.GetContent());
var tabContent = GetTabContentItem(context, output, innerContent.GetContent());
var tabHeaderItems = context.GetValue<List<TabItem>>(TabItems);
@ -30,23 +31,31 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab
var link = TagHelper.Name;
var control = TagHelper.Name;
var title = TagHelper.Title;
var attributes = GetTabHeaderAttributes(context, output);
var classAttributesAsString = attributes.Where(a=>a.Name == "class").ToList().Select(a=>a.Value).JoinAsString(" ");
var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString();
if (!string.IsNullOrWhiteSpace(TagHelper.ParentDropdownName))
{
return "<a class=\"dropdown-item\" id=\"" + id + "\" href=\"#" + link + "\" data-toggle=\"tab\" role=\"tab\" aria-controls=\"" + control + "\" aria-selected=\"false\">" + title + "</a>";
return "<a class=\"dropdown-item "+ classAttributesAsString + "\" id=\"" + id + "\" href=\"#" + link + "\" data-toggle=\"tab\" role=\"tab\" aria-controls=\"" + control + "\" aria-selected=\"false\" "+ otherAttributesAsString + ">" + title + "</a>";
}
return "<li class=\"nav-item\"><a class=\"nav-link" + AbpTabItemActivePlaceholder + "\" id=\"" + id + "\" data-toggle=\"" + TabItemsDataTogglePlaceHolder + "\" href=\"#" + link + "\" role=\"tab\" aria-controls=\"" + control + "\" aria-selected=\"" + AbpTabItemSelectedPlaceholder + "\">" +
return "<li class=\"nav-item\"><a class=\"nav-link " + classAttributesAsString + " " + AbpTabItemActivePlaceholder + "\" id=\"" + id + "\" data-toggle=\"" + TabItemsDataTogglePlaceHolder + "\" href=\"#" + link + "\" role=\"tab\" aria-controls=\"" + control + "\" aria-selected=\"" + AbpTabItemSelectedPlaceholder + "\" "+ otherAttributesAsString + ">" +
title +
"</a></li>";
}
protected virtual string GetTabContentItem(string content)
protected virtual string GetTabContentItem(TagHelperContext context, TagHelperOutput output, string content)
{
var headerId = TagHelper.Name + "-tab";
var id = TagHelper.Name;
var attributes = GetTabContentAttributes(context, output);
var classAttributesAsString = attributes.Where(a => a.Name == "class").ToList().Select(a => a.Name).JoinAsString(" ");
var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString();
return "<div class=\"tab-pane fade" + AbpTabItemShowActivePlaceholder + "\" id=\"" + id + "\" role=\"tabpanel\" aria-labelledby=\"" + headerId + "\">" +
return "<div class=\"tab-pane fade " + classAttributesAsString + " " + AbpTabItemShowActivePlaceholder + "\" id=\"" + id + "\" role=\"tabpanel\" aria-labelledby=\"" + headerId + "\" " + otherAttributesAsString + ">" +
content +
"</div>";
}
@ -58,5 +67,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab
TagHelper.Name = TabItemNamePlaceHolder;
}
}
protected virtual List<TagHelperAttribute> GetTabContentAttributes(TagHelperContext context, TagHelperOutput output) {
var contentprefix = "content-";
return GetTabAttributesByPrefix(output.Attributes, contentprefix);
}
protected virtual List<TagHelperAttribute> GetTabHeaderAttributes(TagHelperContext context, TagHelperOutput output) {
var headerprefix = "header-";
return GetTabAttributesByPrefix(output.Attributes, headerprefix);
}
private List<TagHelperAttribute> GetTabAttributesByPrefix(TagHelperAttributeList attributes, string prefix) {
return attributes.Where(a=>a.Name.StartsWith(prefix))
.Select(a=> new TagHelperAttribute(a.Name.Substring(prefix.Length), a.Value)).ToList();
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml

@ -1,14 +1,17 @@
@using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Components.TenantSwitch
@model TenantSwitchViewComponent.TenantSwitchViewModel
<li class="nav-item">
<a abp-button="Link" id="TenantSwitchToolbarLink" href="#">
@if (Model.Tenant == null)
{
<text>@@host</text>
}
else
{
<text>@@@Model.Tenant.Name</text>
}
</a>
</li>
@if (!Model.CurrentUser.IsAuthenticated)
{
<li class="nav-item">
<a abp-button="Link" id="TenantSwitchToolbarLink" href="#">
@if (Model.Tenant == null)
{
<text>@@host</text>
}
else
{
<text>@@@Model.Tenant.Name</text>
}
</a>
</li>
}

16
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Components.TenantSwitch
{
@ -12,18 +13,25 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Components.TenantSwitch
public const int Order = -1_000_000;
protected ITenantStore TenantStore { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICurrentUser CurrentUser { get; }
public TenantSwitchViewComponent(ITenantStore tenantStore, ICurrentTenant currentTenant)
public TenantSwitchViewComponent(
ITenantStore tenantStore,
ICurrentTenant currentTenant,
ICurrentUser currentUser)
{
TenantStore = tenantStore;
CurrentTenant = currentTenant;
CurrentUser = currentUser;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var model = new TenantSwitchViewModel();
var model = new TenantSwitchViewModel
{
CurrentUser = CurrentUser
};
if (CurrentTenant.Id.HasValue)
{
@ -36,6 +44,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Components.TenantSwitch
public class TenantSwitchViewModel
{
public TenantInfo Tenant { get; set; }
public ICurrentUser CurrentUser { get; set; }
}
}
}

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs

@ -52,7 +52,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
.ScriptBundles
.Add(BasicThemeBundles.Scripts.Global, bundle =>
{
bundle.AddBaseBundles(StandardBundles.Scripts.Global);
bundle
.AddBaseBundles(StandardBundles.Scripts.Global)
.AddContributors(typeof(BasicThemeGlobalScriptContributor));
});
});
}

12
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Bundling/BasicThemeGlobalScriptContributor.cs

@ -0,0 +1,12 @@
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling
{
public class BasicThemeGlobalScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.Add("/themes/basic/layout.js");
}
}
}

47
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml

@ -5,39 +5,36 @@
var elementId = string.IsNullOrEmpty(menuItem.ElementId) ? string.Empty : $"id=\"{menuItem.ElementId}\"";
var cssClass = string.IsNullOrEmpty(menuItem.CssClass) ? string.Empty : menuItem.CssClass;
var disabled = menuItem.IsDisabled ? "disabled" : string.Empty;
if (menuItem.IsLeaf)
{
if (menuItem.Url == null)
@if (menuItem.Url != null)
{
continue;
}
<li class="nav-item @cssClass @disabled" @elementId>
<a class="nav-link" href="@(menuItem.Url ?? "#")">
@if (menuItem.Icon != null)
{
if (menuItem.Icon.StartsWith("fa"))
<li class="nav-item @cssClass @disabled" @elementId>
<a class="nav-link" href="@(menuItem.Url ?? "#")">
@if (menuItem.Icon != null)
{
<i class="@menuItem.Icon"></i>
if (menuItem.Icon.StartsWith("fa"))
{
<i class="@menuItem.Icon"></i>
}
}
}
@menuItem.DisplayName
</a> @*<span class="sr-only">(current)</span>*@
</li>
@menuItem.DisplayName
</a>
</li>
}
}
else
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="Menu_@(menuItem.Name)" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@menuItem.DisplayName</a>
<div class="dropdown-menu" aria-labelledby="Menu_@(menuItem.Name)">
@foreach (var childMenuItem in menuItem.Items)
{
<a class="dropdown-item @cssClass @disabled" href="@(childMenuItem.Url ?? "#")" @Html.Raw(elementId)>
@childMenuItem.DisplayName
</a>
}
<li class="nav-item">
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="Menu_@(menuItem.Name)" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">@menuItem.DisplayName</a>
<div class="dropdown-menu" aria-labelledby="Menu_@(menuItem.Name)">
@foreach (var childMenuItem in menuItem.Items)
{
@await Html.PartialAsync("~/Themes/Basic/Components/Menu/_MenuItem.cshtml", childMenuItem)
}
</div>
</div>
</li>
}
}
}

36
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml

@ -0,0 +1,36 @@
@using Volo.Abp.UI.Navigation
@model ApplicationMenuItem
@{
var elementId = string.IsNullOrEmpty(Model.ElementId) ? string.Empty : $"id=\"{Model.ElementId}\"";
var cssClass = string.IsNullOrEmpty(Model.CssClass) ? string.Empty : Model.CssClass;
var disabled = Model.IsDisabled ? "disabled" : string.Empty;
}
@if (Model.IsLeaf)
{
@if (Model.Url != null)
{
<a class="dropdown-item @cssClass @disabled" href="@(Model.Url ?? "#")" @Html.Raw(elementId)>
@Model.DisplayName
</a>
}
}
else
{
<div class="dropdown-submenu">
<a role="button" class="btn dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="lp-icon">
<i class="@(Model.Icon ?? "")"></i>
</span>
<span class="lp-text">
@Model.DisplayName
</span>
</a>
<div class="dropdown-menu">
@foreach (var childMenuItem in Model.Items)
{
@await Html.PartialAsync("~/Themes/Basic/Components/Menu/_MenuItem.cshtml", childMenuItem)
}
</div>
</div>
}

24
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css

@ -10,3 +10,27 @@ body {
text-decoration: none;
color: #fff;
}
/* Main Menu */
.navbar .dropdown-submenu {
position: relative;
}
.navbar .dropdown-submenu a {
padding: 0.25rem 1.4rem;
}
.navbar .dropdown-submenu a::after {
transform: rotate(-90deg);
position: absolute;
right: 16px;
top: 18px;
}
.navbar .dropdown-submenu .dropdown-menu {
top: 0;
left: 100%;
margin-left: .1rem;
margin-right: .1rem;
}

16
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.js

@ -0,0 +1,16 @@
$(function () {
$('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
if (!$(this).next().hasClass('show')) {
$(this).parents('.dropdown-menu').first().find('.show').removeClass("show");
}
var $subMenu = $(this).next(".dropdown-menu");
$subMenu.toggleClass('show');
$(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function (e) {
$('.dropdown-submenu .show').removeClass("show");
});
return false;
});
});

20
framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs

@ -1,4 +1,7 @@
namespace Volo.Abp.AspNetCore.Mvc.UI.Layout
using System;
using System.Linq;
namespace Volo.Abp.AspNetCore.Mvc.UI.Layout
{
public class ContentLayout
{
@ -12,5 +15,20 @@
{
BreadCrumb = new BreadCrumb();
}
public virtual bool ShouldShowBreadCrumb()
{
if (BreadCrumb.Items.Any())
{
return true;
}
if (BreadCrumb.ShowCurrent && !Title.IsNullOrEmpty())
{
return true;
}
return false;
}
}
}

2
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs

@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Threading
[Dependency(ReplaceServices = true)]
public class HttpContextCancellationTokenProvider : ICancellationTokenProvider, ITransientDependency
{
public CancellationToken Token => _httpContextAccessor.HttpContext?.RequestAborted ?? default;
public CancellationToken Token => _httpContextAccessor.HttpContext?.RequestAborted ?? CancellationToken.None;
private readonly IHttpContextAccessor _httpContextAccessor;

19
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs

@ -27,18 +27,21 @@ namespace Volo.Abp.AspNetCore.Tracing
return CreateNewCorrelationId();
}
lock (HttpContextAccessor.HttpContext.Request.Headers)
{
string correlationId = HttpContextAccessor.HttpContext.Request.Headers[Options.HttpHeaderName];
string correlationId = HttpContextAccessor.HttpContext.Request.Headers[Options.HttpHeaderName];
if (correlationId.IsNullOrEmpty())
if (correlationId.IsNullOrEmpty())
{
lock (HttpContextAccessor.HttpContext.Request.Headers)
{
correlationId = CreateNewCorrelationId();
HttpContextAccessor.HttpContext.Request.Headers[Options.HttpHeaderName] = correlationId;
if (correlationId.IsNullOrEmpty())
{
correlationId = CreateNewCorrelationId();
HttpContextAccessor.HttpContext.Request.Headers[Options.HttpHeaderName] = correlationId;
}
}
return correlationId;
}
return correlationId;
}
protected virtual string CreateNewCorrelationId()

2
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs

@ -9,5 +9,7 @@ namespace Volo.Abp.Authorization.Permissions
PermissionGroupDefinition GetGroupOrNull(string name);
PermissionGroupDefinition AddGroup([NotNull] string name, ILocalizableString displayName = null);
void RemoveGroup(string name);
}
}

11
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs

@ -36,5 +36,16 @@ namespace Volo.Abp.Authorization.Permissions
return Groups[name];
}
public virtual void RemoveGroup(string name)
{
Check.NotNull(name, nameof(name));
if (!Groups.ContainsKey(name))
{
throw new AbpException($"Not found permission group with name: {name}");
}
Groups.Remove(name);
}
}
}

15
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs

@ -15,5 +15,20 @@ namespace Volo.Abp.Caching
Name = name;
}
public static string GetCacheName(Type cacheItemType)
{
var cacheNameAttribute = cacheItemType
.GetCustomAttributes(true)
.OfType<CacheNameAttribute>()
.FirstOrDefault();
if (cacheNameAttribute != null)
{
return cacheNameAttribute.Name;
}
return cacheItemType.FullName.RemovePostFix("CacheItem");
}
}
}

10
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs

@ -127,7 +127,7 @@ namespace Volo.Abp.Caching
return value;
}
using (AsyncLock.Lock())
using (AsyncLock.Lock(CancellationTokenProvider.Token))
{
value = Get(key, hideErrors);
if (value != null)
@ -326,13 +326,7 @@ namespace Volo.Abp.Caching
protected virtual void SetDefaultOptions()
{
//CacheName
var cacheNameAttribute = typeof(TCacheItem)
.GetCustomAttributes(true)
.OfType<CacheNameAttribute>()
.FirstOrDefault();
CacheName = cacheNameAttribute != null ? cacheNameAttribute.Name : typeof(TCacheItem).FullName;
CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem));
//IgnoreMultiTenancy
IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true);

43
framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Volo.Abp.Reflection;
namespace Volo.Abp.Data
{
public static class HasExtraPropertiesExtensions
{
public static bool HasProperty(this IHasExtraProperties source, string name)
{
return source.ExtraProperties.ContainsKey(name);
}
public static object GetProperty(this IHasExtraProperties source, string name)
{
return source.ExtraProperties?.GetOrDefault(name);
}
public static TProperty GetProperty<TProperty>(this IHasExtraProperties source, string name)
{
var value = source.GetProperty(name);
if (value == default)
{
return default;
}
if (TypeHelper.IsPrimitiveExtended(typeof(TProperty), includeEnums: true))
{
return (TProperty)Convert.ChangeType(value, typeof(TProperty), CultureInfo.InvariantCulture);
}
throw new AbpException("GetProperty<TProperty> does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually.");
}
public static TSource SetProperty<TSource>(this TSource source, string name, object value)
where TSource : IHasExtraProperties
{
source.ExtraProperties[name] = value;
return source;
}
}
}

9
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs

@ -75,11 +75,10 @@ namespace Volo.Abp.Domain.Entities
where TEntity : IEntity<TKey>
{
var lambdaParam = Expression.Parameter(typeof(TEntity));
var lambdaBody = Expression.Equal(
Expression.PropertyOrField(lambdaParam, nameof(Entity<TKey>.Id)),
Expression.Constant(id, typeof(TKey))
);
var leftExpression = Expression.PropertyOrField(lambdaParam, "Id");
Expression<Func<object>> closure = () => id;
var rightExpression = Expression.Convert(closure.Body, leftExpression.Type);
var lambdaBody = Expression.Equal(leftExpression, rightExpression);
return Expression.Lambda<Func<TEntity, bool>>(lambdaBody, lambdaParam);
}
}

2
framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs

@ -6,7 +6,7 @@ namespace Volo.Abp.Threading
{
public static NullCancellationTokenProvider Instance { get; } = new NullCancellationTokenProvider();
public CancellationToken Token { get; } = default;
public CancellationToken Token { get; } = CancellationToken.None;
private NullCancellationTokenProvider()
{

7
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs

@ -1,4 +1,4 @@
using Microsoft.AspNetCore;
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo
@ -11,7 +11,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo
}
public static IWebHost BuildWebHostInternal(string[] args) =>
WebHost.CreateDefaultBuilder(args)
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
}

11
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs

@ -1,8 +1,8 @@
using System;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo
{
@ -20,15 +20,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole()
.AddDebug()
.AddSerilog(new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File("Logs/logs.txt")
.CreateLogger()
);
app.InitializeApplication();
}
}

2
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs

@ -50,7 +50,7 @@ namespace Volo.Abp.Authorization
[Fact]
public void Should_Permission_Definition_GetGroup()
{
_permissionDefinitionManager.GetGroups().Count.ShouldBe(2);
_permissionDefinitionManager.GetGroups().Count.ShouldBe(1);
}
}
}

2
framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs

@ -13,6 +13,8 @@ namespace Volo.Abp.Authorization.TestServices
}
PermissionGroupDefinition group = context.AddGroup("TestGroup");
group.AddPermission("MyAuthorizedService1");
context.RemoveGroup("TestGetGroup");
}
}
}

3
framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs

@ -13,13 +13,14 @@ namespace Volo.Abp.Caching
{
option.CacheConfigurators.Add(cacheName =>
{
if (cacheName == typeof(Sail.Testing.Caching.PersonCacheItem).FullName)
if (cacheName == CacheNameAttribute.GetCacheName(typeof(Sail.Testing.Caching.PersonCacheItem)))
{
return new DistributedCacheEntryOptions()
{
AbsoluteExpiration = DateTime.Parse("2099-01-01 12:00:00")
};
}
return null;
});

13
framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs

@ -1,9 +1,7 @@
using Microsoft.Extensions.Caching.Distributed;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Xunit;
@ -12,20 +10,11 @@ namespace Volo.Abp.Caching
public class DistributedCache_ConfigureOptions_Test : AbpIntegratedTest<AbpCachingTestModule>
{
[Fact]
public async Task Configure_CacheOptions()
public void Configure_CacheOptions()
{
var personCache = GetRequiredService<IDistributedCache<Sail.Testing.Caching.PersonCacheItem>>();
var cacheKey = Guid.NewGuid().ToString();
//Get (not exists yet)
var cacheItem = await personCache.GetAsync(cacheKey);
cacheItem.ShouldBeNull();
GetDefaultCachingOptions(personCache).SlidingExpiration.ShouldBeNull();
GetDefaultCachingOptions(personCache).AbsoluteExpiration.ShouldBe(new DateTime(2099, 1, 1, 12, 0, 0));
}
[Fact]

9
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs

@ -0,0 +1,9 @@
using Volo.Abp.TestApp.Testing;
namespace Volo.Abp.EntityFrameworkCore.Domain
{
public class ExtraProperties_Tests : ExtraProperties_Tests<AbpEntityFrameworkCoreTestModule>
{
}
}

9
framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ExtraProperties_Tests.cs

@ -0,0 +1,9 @@
using Volo.Abp.TestApp.Testing;
namespace Volo.Abp.MongoDB.Domain
{
public class ExtraProperties_Tests : ExtraProperties_Tests<AbpMongoDbTestModule>
{
}
}

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs

@ -36,7 +36,7 @@ namespace Volo.Abp.TestApp
{
_cityRepository.Insert(new City(Guid.NewGuid(), "Tokyo"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Madrid"));
_cityRepository.Insert(new City(LondonCityId, "London"));
_cityRepository.Insert(new City(LondonCityId, "London") {ExtraProperties = { { "Population", 10_470_000 } } });
_cityRepository.Insert(new City(IstanbulCityId, "Istanbul"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Paris"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Washington"));

53
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs

@ -0,0 +1,53 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.TestApp.Domain;
using Xunit;
namespace Volo.Abp.TestApp.Testing
{
public abstract class ExtraProperties_Tests<TStartupModule> : TestAppTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected readonly ICityRepository CityRepository;
protected ExtraProperties_Tests()
{
CityRepository = GetRequiredService<ICityRepository>();
}
[Fact]
public async Task Should_Get_An_Extra_Property()
{
var london = await CityRepository.FindByNameAsync("London");
london.HasProperty("Population").ShouldBeTrue();
london.GetProperty<int>("Population").ShouldBe(10_470_000);
}
[Fact]
public async Task Should_Add_An_Extra_Property()
{
var london = await CityRepository.FindByNameAsync("London");
london.SetProperty("AreaAsKm", 1572);
await CityRepository.UpdateAsync(london);
var london2 = await CityRepository.FindByNameAsync("London");
london2.HasProperty("AreaAsKm").ShouldBeTrue();
london2.GetProperty<int>("AreaAsKm").ShouldBe(1572);
}
[Fact]
public async Task Should_Update_An_Existing_Extra_Property()
{
var london = await CityRepository.FindByNameAsync("London");
london.ExtraProperties["Population"] = 11_000_042;
await CityRepository.UpdateAsync(london);
var london2 = await CityRepository.FindByNameAsync("London");
london2.HasProperty("Population").ShouldBeTrue();
london2.GetProperty<int>("Population").ShouldBe(11_000_042);
}
}
}

9
modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs

@ -1,5 +1,6 @@
using Volo.Abp.IdentityServer;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Account.Web
{
@ -9,6 +10,12 @@ namespace Volo.Abp.Account.Web
)]
public class AbpAccountWebIdentityServerModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<VirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpAccountWebIdentityServerModule>("Volo.Abp.Account.Web");
});
}
}
}

113
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml

@ -0,0 +1,113 @@
@page
@using Volo.Abp.Account.Web.Pages
@using Volo.Abp.Account.Web.Pages.Account
@model ConsentModel
<abp-card id="IdentityServerConsentWrapper">
<abp-card-header>
<div class="row">
<div class="col-md-12">
<h2>
@if (Model.ClientInfo.ClientLogoUrl != null)
{
<img src="@Model.ClientInfo.ClientLogoUrl">
}
@Model.ClientInfo.ClientName
<small>is requesting your permission</small>
</h2>
</div>
</div>
</abp-card-header>
<abp-card-body>
<form method="post" asp-page="/Consent">
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="ReturnUrlHash" />
<div>Uncheck the permissions you do not wish to grant.</div>
@if (Model.ConsentInput.IdentityScopes.Any())
{
<h3>Personal Information</h3>
<ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++)
{
<li class="list-group-item">
<div class="form-check">
<label asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-input" />
@Model.ConsentInput.IdentityScopes[i].DisplayName
@if (Model.ConsentInput.IdentityScopes[i].Required)
{
<span><em>(required)</em></span>
}
</label>
</div>
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.IdentityScopes[i].Description != null)
{
<div class="consent-description">
@Model.ConsentInput.IdentityScopes[i].Description
</div>
}
</li>
}
</ul>
}
@if (Model.ConsentInput.ApiScopes.Any())
{
<h3>Application Access</h3>
<ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++)
{
<li class="list-group-item">
<div class="form-check">
<label asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-input" disabled="@Model.ConsentInput.ApiScopes[i].Required" />
@Model.ConsentInput.ApiScopes[i].DisplayName
@if (Model.ConsentInput.ApiScopes[i].Required)
{
<span><em>(required)</em></span>
}
</label>
</div>
<input asp-for="@Model.ConsentInput.ApiScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.ApiScopes[i].Description != null)
{
<div class="consent-description">
@Model.ConsentInput.ApiScopes[i].Description
</div>
}
</li>
}
</ul>
}
@if (Model.ClientInfo.AllowRememberConsent)
{
<div class="form-check">
<label asp-for="@Model.ConsentInput.RememberConsent" class="form-check-label">
<input asp-for="@Model.ConsentInput.RememberConsent" class="form-check-input" />
<strong>Remember My Decision</strong>
</label>
</div>
}
<div>
<button name="UserDecision" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="UserDecision" value="no" class="btn">No, Do Not Allow</button>
@if (Model.ClientInfo.ClientUrl != null)
{
<a class="pull-right btn btn-secondary" target="_blank" href="@Model.ClientInfo.ClientUrl">
<strong>@Model.ClientInfo.ClientName</strong>
</a>
}
</div>
<div asp-validation-summary="All" class="text-danger"></div>
</form>
</abp-card-body>
</abp-card>

240
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs

@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.UI;
namespace Volo.Abp.Account.Web.Pages
{
//TODO: Move this into the Account folder!!!
public class ConsentModel : AbpPageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public string ReturnUrlHash { get; set; }
[BindProperty]
public ConsentModel.ConsentInputModel ConsentInput { get; set; }
public ClientInfoModel ClientInfo { get; set; }
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
public ConsentModel(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IResourceStore resourceStore)
{
_interaction = interaction;
_clientStore = clientStore;
_resourceStore = resourceStore;
}
public virtual async Task OnGet()
{
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (request == null)
{
throw new ApplicationException($"No consent request matching request: {ReturnUrl}");
}
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
if (client == null)
{
throw new ApplicationException($"Invalid client id: {request.ClientId}");
}
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources == null || (!resources.IdentityResources.Any() && !resources.ApiResources.Any()))
{
throw new ApplicationException($"No scopes matching: {request.ScopesRequested.Aggregate((x, y) => x + ", " + y)}");
}
ClientInfo = new ClientInfoModel(client);
ConsentInput = new ConsentInputModel
{
RememberConsent = true,
IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, true)).ToList(),
ApiScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, true)).ToList()
};
if (resources.OfflineAccess)
{
ConsentInput.ApiScopes.Add(GetOfflineAccessScope(true));
}
}
public virtual async Task<IActionResult> OnPost(string userDecision)
{
var result = await ProcessConsentAsync();
if (result.IsRedirect)
{
return Redirect(result.RedirectUri);
}
if (result.HasValidationError)
{
//ModelState.AddModelError("", result.ValidationError);
throw new ApplicationException("Error: " + result.ValidationError);
}
throw new ApplicationException("Unknown Error!");
}
protected virtual async Task<ConsentModel.ProcessConsentResult> ProcessConsentAsync()
{
var result = new ConsentModel.ProcessConsentResult();
ConsentResponse grantedConsent;
if (ConsentInput.UserDecision == "no")
{
grantedConsent = ConsentResponse.Denied;
}
else
{
if (ConsentInput.IdentityScopes.Any() || ConsentInput.ApiScopes.Any())
{
grantedConsent = new ConsentResponse
{
RememberConsent = ConsentInput.RememberConsent,
ScopesConsented = ConsentInput.GetAllowedScopeNames()
};
}
else
{
throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this
}
}
if (grantedConsent != null)
{
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (request == null)
{
return result;
}
await _interaction.GrantConsentAsync(request, grantedConsent);
result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash?
}
return result;
}
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = identity.Name,
DisplayName = identity.DisplayName,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description,
Emphasize = scope.Emphasize,
Required = scope.Required,
Checked = check || scope.Required
};
}
protected virtual ConsentModel.ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ConsentModel.ScopeViewModel
{
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = "Offline Access", //TODO: Localize
Description = "Access to your applications and resources, even when you are offline",
Emphasize = true,
Checked = check
};
}
public class ConsentInputModel
{
public List<ConsentModel.ScopeViewModel> IdentityScopes { get; set; }
public List<ConsentModel.ScopeViewModel> ApiScopes { get; set; }
[Required]
public string UserDecision { get; set; }
public bool RememberConsent { get; set; }
public List<string> GetAllowedScopeNames()
{
return IdentityScopes.Union(ApiScopes).Where(s => s.Checked).Select(s => s.Name).ToList();
}
}
public class ScopeViewModel
{
[Required]
[HiddenInput]
public string Name { get; set; }
public bool Checked { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
}
public class ProcessConsentResult
{
public bool IsRedirect => RedirectUri != null;
public string RedirectUri { get; set; }
public bool HasValidationError => ValidationError != null;
public string ValidationError { get; set; }
}
public class ClientInfoModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public ClientInfoModel(Client client)
{
//TODO: Automap
ClientName = client.ClientId;
ClientUrl = client.ClientUri;
ClientLogoUrl = client.LogoUri;
AllowRememberConsent = client.AllowRememberConsent;
}
}
}
}

4
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj

@ -20,8 +20,10 @@
</ItemGroup>
<ItemGroup>
<Content Remove="Pages\**\*.cshtml" />
<Content Remove="Pages\**\*.css" />
<Content Remove="Pages\**\*.js" />
<Content Remove="Properties\launchSettings.json" />
<EmbeddedResource Remove="Pages\Account\IdentityServerSupportedLoginModel.cs" />
<None Include="Properties\launchSettings.json" />
</ItemGroup>

1
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml

@ -3,6 +3,7 @@
@model Volo.Abp.Account.Web.Pages.Account.LoginModel
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
<h2>@L["Login"]</h2>
@if (Model.EnableLocalLogin)
{
<form method="post">

7
modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs

@ -31,6 +31,7 @@ using Volo.Abp.Threading;
using Volo.Abp.UI;
using Volo.Abp.VirtualFileSystem;
using Volo.Blogging;
using Volo.Blogging.Files;
using Volo.BloggingTestApp.EntityFrameworkCore;
using Volo.BloggingTestApp.MongoDb;
@ -109,6 +110,12 @@ namespace Volo.BloggingTestApp
{
options.DefaultThemeName = BasicTheme.Name;
});
Configure<BlogFileOptions>(options =>
{
options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files");
options.FileUploadUrlRoot = "/files/";
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)

9
modules/blogging/app/Volo.BloggingTestApp/Startup.cs

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Volo.Abp;
namespace Volo.BloggingTestApp
@ -22,14 +21,6 @@ namespace Volo.BloggingTestApp
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole()
.AddDebug()
.AddSerilog(new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File("Logs/logs.txt")
.CreateLogger()
);
app.InitializeApplication();
}

1
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs

@ -11,7 +11,6 @@
public const string Delete = Default + ".Delete";
public const string Update = Default + ".Update";
public const string Create = Default + ".Create";
}
public static class Posts

2
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs

@ -8,8 +8,6 @@ namespace Volo.Blogging.Blogs
{
public interface IBlogAppService : IApplicationService
{
Task<PagedResultDto<BlogDto>> GetListPagedAsync(PagedAndSortedResultRequestDto input);
Task<ListResultDto<BlogDto>> GetListAsync();
Task<BlogDto> GetByShortNameAsync(string shortName);

14
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs

@ -0,0 +1,14 @@
using System;
namespace Volo.Blogging
{
public class BloggingWebConsts
{
public class FileUploading
{
public const int MaxFileSize = 5242880; //5MB
public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f);
}
}
}

13
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Volo.Blogging.Files
{
public class FileUploadInputDto
{
[Required]
public byte[] Bytes { get; set; }
[Required]
public string Name { get; set; }
}
}

7
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs

@ -0,0 +1,7 @@
namespace Volo.Blogging.Files
{
public class FileUploadOutputDto
{
public string Url { get; set; }
}
}

10
modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Blogging.Files
{
public interface IFileAppService : IApplicationService
{
Task<FileUploadOutputDto> UploadAsync(FileUploadInputDto input);
}
}

1
modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj

@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="4.5.0" />
<ProjectReference Include="..\Volo.Blogging.Application.Contracts\Volo.Blogging.Application.Contracts.csproj" />
<ProjectReference Include="..\Volo.Blogging.Domain\Volo.Blogging.Domain.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />

11
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs

@ -18,17 +18,6 @@ namespace Volo.Blogging.Blogs
_blogRepository = blogRepository;
}
public async Task<PagedResultDto<BlogDto>> GetListPagedAsync(PagedAndSortedResultRequestDto input)
{
var blogs = await _blogRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount );
var totalCount = await _blogRepository.GetTotalCount();
var dtos = ObjectMapper.Map<List<Blog>, List<BlogDto>>(blogs);
return new PagedResultDto<BlogDto>(totalCount, dtos);
}
public async Task<ListResultDto<BlogDto>> GetListAsync()
{
var blogs = await _blogRepository.GetListAsync();

5
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Abp.Guids;
using Volo.Abp.Users;
using Volo.Blogging.Comments.Dtos;
using Volo.Blogging.Posts;
using Volo.Blogging.Users;
@ -81,7 +80,7 @@ namespace Volo.Blogging.Comments
ObjectMapper.Map<List<Comment>, List<CommentWithDetailsDto>>(comments));
}
//[Authorize(BloggingPermissions.Comments.Create)] TODO: Temporary removed
[Authorize]
public async Task<CommentWithDetailsDto> CreateAsync(CreateCommentDto input)
{
var comment = new Comment(_guidGenerator.Create(), input.PostId, input.RepliedCommentId, input.Text);
@ -91,6 +90,7 @@ namespace Volo.Blogging.Comments
return ObjectMapper.Map<Comment, CommentWithDetailsDto>(comment);
}
[Authorize]
public async Task<CommentWithDetailsDto> UpdateAsync(Guid id, UpdateCommentDto input)
{
var comment = await _commentRepository.GetAsync(id);
@ -104,6 +104,7 @@ namespace Volo.Blogging.Comments
return ObjectMapper.Map<Comment, CommentWithDetailsDto>(comment);
}
[Authorize]
public async Task DeleteAsync(Guid id)
{
var comment = await _commentRepository.GetAsync(id);

5
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Volo.Blogging
{

13
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs

@ -0,0 +1,13 @@
namespace Volo.Blogging.Files
{
/* TODO:
* - It is not to have different options for all different modules. We should find a more generic way.
* - Actually, it is not good to assume to save to a local folder. Instead, use file storage once implemented.
*/
public class BlogFileOptions
{
public string FileUploadLocalFolder { get; set; }
public string FileUploadUrlRoot { get; set; }
}
}

65
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Validation;
using Volo.Blogging.Areas.Blog.Helpers;
namespace Volo.Blogging.Files
{
public class FileAppService : ApplicationService, IFileAppService
{
public BlogFileOptions Options { get; }
public FileAppService(IOptions<BlogFileOptions> options)
{
Options = options.Value;
}
public virtual Task<FileUploadOutputDto> UploadAsync(FileUploadInputDto input)
{
if (input.Bytes.IsNullOrEmpty())
{
ThrowValidationException("Bytes can not be null or empty!", "Bytes");
}
if (input.Bytes.Length > BloggingWebConsts.FileUploading.MaxFileSize)
{
throw new UserFriendlyException($"File exceeds the maximum upload size ({BloggingWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!");
}
if (!ImageFormatHelper.IsValidImage(input.Bytes, FileUploadConsts.AllowedImageUploadFormats))
{
throw new UserFriendlyException("Not a valid image format!");
}
var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(input.Name));
var filePath = Path.Combine(Options.FileUploadLocalFolder, uniqueFileName);
File.WriteAllBytes(filePath, input.Bytes); //TODO: Previously was using WriteAllBytesAsync, but it's only in .netcore.
return Task.FromResult(new FileUploadOutputDto
{
Url = Options.FileUploadUrlRoot.EnsureEndsWith('/') + uniqueFileName
});
}
private static void ThrowValidationException(string message, string memberName)
{
throw new AbpValidationException(message,
new List<ValidationResult>
{
new ValidationResult(message, new[] {memberName})
});
}
protected virtual string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null)
{
return prefix + GuidGenerator.Create().ToString("N") + postfix + extension;
}
}
}

20
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing.Imaging;
using System.Linq;
namespace Volo.Blogging.Files
{
public class FileUploadConsts
{
public static readonly ICollection<ImageFormat> AllowedImageUploadFormats = new Collection<ImageFormat>
{
ImageFormat.Jpeg,
ImageFormat.Png,
ImageFormat.Gif,
ImageFormat.Bmp
};
public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString()));
}
}

0
modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Helpers/ImageFormatHelper.cs → modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs

41
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs

@ -13,11 +13,6 @@ using Volo.Blogging.Users;
namespace Volo.Blogging.Posts
{
/* TODO: Custom policy with configuration.
* We should create a custom policy to see the blog as read only if the blog is
* configured as 'public' or the current user has the related permission.
*/
//[Authorize(BloggingPermissions.Posts.Default)]
public class PostAppService : ApplicationService, IPostAppService
{
protected IBlogUserLookupService UserLookupService { get; }
@ -79,24 +74,6 @@ namespace Volo.Blogging.Posts
return new ListResultDto<PostWithDetailsDto>(postDtos);
}
private async Task<List<PostWithDetailsDto>> FilterPostsByTag(List<PostWithDetailsDto> allPostDtos, Tag tag)
{
var filteredPostDtos = new List<PostWithDetailsDto>();
var posts = await _postRepository.GetListAsync();
foreach (var postDto in allPostDtos)
{
if (!postDto.Tags.Any(p=> p.Id == tag.Id))
{
continue;
}
filteredPostDtos.Add(postDto);
}
return filteredPostDtos;
}
public async Task<PostWithDetailsDto> GetForReadingAsync(GetPostInput input)
{
var post = await _postRepository.GetPostByUrl(input.BlogId, input.Url);
@ -135,6 +112,7 @@ namespace Volo.Blogging.Posts
return postDto;
}
[Authorize(BloggingPermissions.Posts.Delete)]
public async Task DeleteAsync(Guid id)
{
var post = await _postRepository.GetAsync(id);
@ -272,5 +250,22 @@ namespace Volo.Blogging.Posts
}
return new List<string>(tags.Split(",").Select(t => t.Trim()));
}
private Task<List<PostWithDetailsDto>> FilterPostsByTag(List<PostWithDetailsDto> allPostDtos, Tag tag)
{
var filteredPostDtos = new List<PostWithDetailsDto>();
foreach (var postDto in allPostDtos)
{
if (postDto.Tags.All(p => p.Id != tag.Id))
{
continue;
}
filteredPostDtos.Add(postDto);
}
return Task.FromResult(filteredPostDtos);
}
}
}

7
modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs

@ -2,17 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Services;
using Volo.Blogging.Tagging.Dtos;
namespace Volo.Blogging.Tagging
{
/* TODO: Custom policy with configuration.
* We should create a custom policy to see the blog as read only if the blog is
* configured as 'public' or the current user has the related permission.
*/
//[Authorize(BloggingPermissions.Tags.Default)]
public class TagAppService : ApplicationService, ITagAppService
{
private readonly ITagRepository _tagRepository;
@ -28,7 +22,6 @@ namespace Volo.Blogging.Tagging
.WhereIf(input.MinimumPostCount != null, t=>t.UsageCount >= input.MinimumPostCount)
.Take(input.ResultCount).ToList();
return new List<TagDto>(
ObjectMapper.Map<List<Tag>, List<TagDto>>(postTags));
}

3
modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
@ -9,8 +8,6 @@ namespace Volo.Blogging.Blogs
{
Task<Blog> FindByShortNameAsync(string shortName);
Task<List<Blog>> GetListAsync(string sorting, int maxResultCount, int skipCount);
Task<int> GetTotalCount();
}
}

46
modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs

@ -4,7 +4,7 @@ using Volo.Abp.Users;
namespace Volo.Blogging.Users
{
public class BlogUser : AggregateRoot<Guid>, IUser
public class BlogUser : AggregateRoot<Guid>, IUser, IUpdateUserData
{
public virtual Guid? TenantId { get; protected set; }
@ -30,17 +30,45 @@ namespace Volo.Blogging.Users
public BlogUser(IUserData user)
: base(user.Id)
{
Email = user.Email;
Name = user.Name;
Surname = user.Surname;
EmailConfirmed = user.EmailConfirmed;
PhoneNumber = user.PhoneNumber;
PhoneNumberConfirmed = user.PhoneNumberConfirmed;
UserName = user.UserName;
TenantId = user.TenantId;
UpdateInternal(user);
}
public virtual bool Update(IUserData user)
{
if (Id != user.Id)
{
throw new ArgumentException($"Given User's Id '{user.Id}' does not match to this User's Id '{Id}'");
}
if (TenantId != user.TenantId)
{
throw new ArgumentException($"Given User's TenantId '{user.TenantId}' does not match to this User's TenantId '{TenantId}'");
}
if (Equals(user))
{
return false;
}
UpdateInternal(user);
return true;
}
protected virtual bool Equals(IUserData user)
{
return Id == user.Id &&
TenantId == user.TenantId &&
UserName == user.UserName &&
Name == user.Name &&
Surname == user.Surname &&
Email == user.Email &&
EmailConfirmed == user.EmailConfirmed &&
PhoneNumber == user.PhoneNumber &&
PhoneNumberConfirmed == user.PhoneNumberConfirmed;
}
public void Update(IUserData user)
protected virtual void UpdateInternal(IUserData user)
{
Email = user.Email;
Name = user.Name;

1
modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs

@ -12,6 +12,7 @@ namespace Volo.Blogging.Users
userRepository,
unitOfWorkManager)
{
}
protected override BlogUser CreateUser(IUserData externalUser)

12
modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
@ -23,15 +20,6 @@ namespace Volo.Blogging.Blogs
return await DbSet.FirstOrDefaultAsync(p => p.ShortName == shortName);
}
public async Task<List<Blog>> GetListAsync(string sorting, int maxResultCount, int skipCount)
{
var auditLogs = await DbSet.OrderBy(sorting ?? "creationTime desc")
.PageBy(skipCount, maxResultCount)
.ToListAsync();
return auditLogs;
}
public async Task<int> GetTotalCount()
{
return await DbSet.CountAsync();

8
modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs

@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Auditing;
using Volo.Blogging.Blogs;
using Volo.Blogging.Blogs.Dtos;
@ -23,13 +22,6 @@ namespace Volo.Blogging
}
[HttpGet]
public async Task<PagedResultDto<BlogDto>> GetListPagedAsync(PagedAndSortedResultRequestDto input)
{
return await _blogAppService.GetListPagedAsync(input);
}
[HttpGet]
[Route("all")]
public async Task<ListResultDto<BlogDto>> GetListAsync()
{
return await _blogAppService.GetListAsync();

4
modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj

@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Blogging.MongoDB</AssemblyName>
<PackageId>Volo.Blogging.MongoDB</PackageId>
<RootNamespace />

12
modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
using Volo.Blogging.MongoDB;
using System.Linq;
using System.Linq.Dynamic.Core;
namespace Volo.Blogging.Blogs
{
@ -21,15 +18,6 @@ namespace Volo.Blogging.Blogs
return await GetMongoQueryable().FirstOrDefaultAsync(p => p.ShortName == shortName);
}
public async Task<List<Blog>> GetListAsync(string sorting, int maxResultCount, int skipCount)
{
var auditLogs = GetMongoQueryable().OrderBy(sorting ?? "creationTime desc").As<IMongoQueryable<Blog>>()
.PageBy(skipCount, maxResultCount)
.ToList();
return auditLogs;
}
public async Task<int> GetTotalCount()
{
return await GetMongoQueryable().CountAsync();

36
modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs

@ -1,31 +1,55 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Blogging.Areas.Blog.Models;
using Volo.Blogging.Files;
using Volo.Blogging.Hosting;
namespace Volo.Blogging.Areas.Blog.Controllers
{
//TODO: This may be moved to HttpApi project since it may be needed by a SPA too.
[Area("Blog")]
[Route("Blog/[controller]/[action]")]
public class FilesController : AbpController
{
private readonly IFileService _fileService;
private readonly IFileAppService _fileAppService;
public FilesController(IFileService fileService)
public FilesController(IFileAppService fileAppService)
{
_fileService = fileService;
_fileAppService = fileAppService;
}
[HttpPost]
public async Task<JsonResult> UploadImage(IFormFile file)
{
file.ValidateImage(out var fileBytes);
//TODO: localize exception messages
var fileUrl = await _fileService.SaveFileAsync(fileBytes, file.FileName);
if (file == null)
{
throw new UserFriendlyException("No file found!");
}
return Json(new FileUploadResult(fileUrl));
if (file.Length <= 0)
{
throw new UserFriendlyException("File is empty!");
}
if (!file.ContentType.Contains("image"))
{
throw new UserFriendlyException("Not a valid image!");
}
var output = await _fileAppService.UploadAsync(
new FileUploadInputDto
{
Bytes = file.AsBytes(),
Name = file.FileName
}
);
return Json(new FileUploadResult(output.Url));
}
}
}

30
modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing.Imaging;
using System.Linq;
namespace Volo.Blogging
{
public class BloggingWebConsts
{
public class FileUploading
{
public const string DefaultFileUploadFolderName = "files";
public static readonly ICollection<ImageFormat> AllowedImageUploadFormats = new Collection<ImageFormat>
{
ImageFormat.Jpeg,
ImageFormat.Png,
ImageFormat.Gif,
ImageFormat.Bmp
};
public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString()));
public const int MaxFileSize = 5242880; //5MB
public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f);
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save