Browse Source

Merge pull request #1 from abpframework/master

0.14.0
pull/1170/head
Mon 8 years ago
committed by GitHub
parent
commit
d67b1b82c4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 0
      .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. 54
      docs/en/Blog-Posts/2019-02-22/Post.md
  14. BIN
      docs/en/Blog-Posts/2019-02-22/scott-and-jon.png
  15. 21
      docs/en/Getting-Started-AspNetCore-Application.md
  16. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml
  17. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs
  18. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs
  19. 12
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Bundling/BasicThemeGlobalScriptContributor.cs
  20. 47
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml
  21. 36
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml
  22. 24
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css
  23. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.js
  24. 20
      framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs
  25. 2
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs
  26. 19
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs
  27. 2
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
  28. 11
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
  29. 15
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs
  30. 10
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
  31. 43
      framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs
  32. 9
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
  33. 2
      framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs
  34. 7
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs
  35. 11
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs
  36. 2
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs
  37. 2
      framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs
  38. 3
      framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs
  39. 13
      framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs
  40. 9
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs
  41. 9
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ExtraProperties_Tests.cs
  42. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs
  43. 53
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs
  44. 9
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs
  45. 113
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml
  46. 240
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs
  47. 4
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj
  48. 1
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml
  49. 7
      modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs
  50. 9
      modules/blogging/app/Volo.BloggingTestApp/Startup.cs
  51. 1
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs
  52. 2
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs
  53. 14
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs
  54. 13
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs
  55. 7
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs
  56. 10
      modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs
  57. 1
      modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj
  58. 11
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs
  59. 5
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs
  60. 5
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs
  61. 13
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs
  62. 65
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs
  63. 20
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs
  64. 0
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs
  65. 41
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs
  66. 7
      modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs
  67. 3
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs
  68. 46
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs
  69. 1
      modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs
  70. 12
      modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs
  71. 8
      modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs
  72. 4
      modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj
  73. 12
      modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs
  74. 36
      modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs
  75. 30
      modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs
  76. 72
      modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs
  77. 42
      modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs
  78. 16
      modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs
  79. 2
      modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs
  80. 1
      modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs
  81. 2
      modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js
  82. 1
      modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj
  83. 31
      modules/docs/app/Volo.DocsTestApp/Program.cs
  84. 10
      modules/docs/app/Volo.DocsTestApp/Startup.cs
  85. 2
      modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj
  86. 2
      modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs
  87. 5
      modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs
  88. 7
      modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs
  89. 8
      modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs
  90. 2
      modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs
  91. 4
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs
  92. 4
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/FileSystem/Documents/FileSystemDocumentStore.cs
  93. 37
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs
  94. 32
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml
  95. 19
      modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs
  96. 3
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/AbpIdentityDomainSharedModule.cs
  97. 10
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs
  98. 1
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs
  99. 1
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityDataSeeder_Tests.cs
  100. 1
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj

0
samples/MicroserviceDemo/.dockerignore → .dockerignore

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

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).

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);
}
}
}

72
modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs

@ -1,72 +0,0 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Volo.Abp;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
namespace Volo.Blogging.Hosting
{
public class FileService : IFileService, ITransientDependency
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IGuidGenerator _guidGenerator;
public FileService(IHostingEnvironment hostingEnvironment, IGuidGenerator guidGenerator)
{
_hostingEnvironment = hostingEnvironment;
_guidGenerator = guidGenerator;
}
public string FileUploadDirectory
{
get
{
var uploadDirectory = Path.Combine(_hostingEnvironment.WebRootPath, BloggingWebConsts.FileUploading.DefaultFileUploadFolderName);
if (!Directory.Exists(uploadDirectory))
{
Directory.CreateDirectory(uploadDirectory);
}
return uploadDirectory;
}
}
public string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null)
{
return prefix + _guidGenerator.Create().ToString("N") + postfix + extension;
}
public async Task<string> SaveFormFileAndGetUrlAsync(IFormFile file)
{
var uniqueFileName = await SaveFileInternalAsync(file.FileName, file.AsBytes());
return GetFileUrl(uniqueFileName);
}
public async Task<string> SaveFileAsync(byte[] fileBytes, string originalFileName)
{
if (fileBytes == null || fileBytes.Length == 0)
{
throw new UserFriendlyException("File is empty!");
}
var uniqueFileName = await SaveFileInternalAsync(originalFileName, fileBytes);
return GetFileUrl(uniqueFileName);
}
private static string GetFileUrl(string uniqueFileName)
{
return "/" + BloggingWebConsts.FileUploading.DefaultFileUploadFolderName + "/" + uniqueFileName;
}
private async Task<string> SaveFileInternalAsync(string originalFileName, byte[] fileBytes)
{
var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(originalFileName));
var filePath = Path.Combine(FileUploadDirectory, uniqueFileName);
File.WriteAllBytes(filePath, fileBytes); //TODO: Previously was using WriteAllBytesAsync, but it's only in .netcore.
return uniqueFileName;
}
}
}

42
modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs

@ -2,56 +2,22 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Volo.Abp;
using Volo.Blogging.Areas.Blog.Helpers;
namespace Volo.Blogging.Hosting
{
public static class FormFileExtensions
{
public static byte[] AsBytes(this IFormFile file)
public static byte[] AsBytes(this IFormFile file) //TODO: Move to the framework (rename to GetBytes)
{
byte[] fileBytes;
using (var stream = file.OpenReadStream())
{
fileBytes = stream.GetAllBytes();
return stream.GetAllBytes();
}
return fileBytes;
}
public static void ValidateImage([CanBeNull] this IFormFile file, out byte[] fileBytes)
public static void ValidateImage([CanBeNull] this IFormFile file)
{
fileBytes = null;
if (file == null)
{
throw new UserFriendlyException("No file found!");
}
if (file.Length <= 0)
{
throw new UserFriendlyException("File is empty!");
}
if (!file.ContentType.Contains("image"))
{
throw new UserFriendlyException("Not a valid image!");
}
using (var stream = file.OpenReadStream())
{
fileBytes = stream.GetAllBytes();
}
if (!ImageFormatHelper.IsValidImage(fileBytes, BloggingWebConsts.FileUploading.AllowedImageUploadFormats))
{
throw new UserFriendlyException("Not a valid image format!");
}
if (file.Length > BloggingWebConsts.FileUploading.MaxFileSize)
{
throw new UserFriendlyException($"File exceeds the maximum upload size ({BloggingWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!");
}
}
}
}

16
modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs

@ -1,16 +0,0 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Volo.Blogging.Hosting
{
public interface IFileService
{
string FileUploadDirectory { get; }
string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null);
Task<string> SaveFormFileAndGetUrlAsync(IFormFile file);
Task<string> SaveFileAsync(byte[] fileBytes, string originalFileName);
}
}

2
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs

@ -28,7 +28,7 @@ namespace Volo.Blogging.Pages.Blog
return title;
}
public string GetShortContent(string content)
public string GetShortContent(string content) //TODO: This should be moved to its own place!
{
var openingTag = "<p>";
var closingTag = "</p>";

1
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Blogging.Blogs;
using Volo.Blogging.Blogs.Dtos;

2
modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js

@ -52,7 +52,6 @@
});
};
console.log($form.find("input[name='Post.Content']").val() + "asda");
var newPostEditor = $editorContainer.tuiEditor({
usageStatistics: false,
initialEditType: 'markdown',
@ -82,7 +81,6 @@
var postText = newPostEditor.getMarkdown();
$postTextInput.val(postText);
console.log(postText);
$submitButton.buttonBusy();
$(this).off('submit').submit();

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

@ -19,7 +19,6 @@
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\Volo.Blogging.HttpApi\Volo.Blogging.HttpApi.csproj" />
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
<PackageReference Include="System.Drawing.Common" Version="4.5.0" />
</ItemGroup>
<ItemGroup>

31
modules/docs/app/Volo.DocsTestApp/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.DocsTestApp
{
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();
}
}
public static IWebHost BuildWebHostInternal(string[] args) =>
@ -16,6 +40,7 @@ namespace Volo.DocsTestApp
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseSerilog()
.Build();
}
}

10
modules/docs/app/Volo.DocsTestApp/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.DocsTestApp
@ -22,15 +21,6 @@ namespace Volo.DocsTestApp
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();
}
}

2
modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj

@ -11,7 +11,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="Swashbuckle.AspNetCore" Version="2.1.0" />
</ItemGroup>

2
modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs

@ -14,7 +14,7 @@ namespace Volo.Docs.Admin
CreateMap<EditModel.EditGithubProjectViewModel, UpdateProjectDto>().Ignore(x => x.ExtraProperties);
CreateMap<ProjectDto, EditModel.EditGithubProjectViewModel > ()
.Ignore(x => x.GitHubAccessToken).Ignore(x => x.GitHubRootUrl);
.Ignore(x => x.GitHubAccessToken).Ignore(x => x.GitHubRootUrl).Ignore(x => x.GitHubUserAgent);
}
}
}

5
modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs

@ -60,6 +60,7 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
dto.ExtraProperties = new Dictionary<string, object>
{
{nameof(GithubProject.GitHubRootUrl), GithubProject.GitHubRootUrl},
{nameof(GithubProject.GitHubUserAgent), GithubProject.GitHubUserAgent},
{nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken}
};
@ -109,6 +110,10 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
[DisplayOrder(10001)]
[StringLength(512)]
public string GitHubAccessToken { get; set; }
[DisplayOrder(10002)]
[StringLength(64)]
public string GitHubUserAgent { get; set; }
}
}
}

7
modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs

@ -62,6 +62,7 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
dto.ExtraProperties = new Dictionary<string, object>
{
{nameof(GithubProject.GitHubRootUrl), GithubProject.GitHubRootUrl},
{nameof(GithubProject.GitHubUserAgent), GithubProject.GitHubUserAgent},
{nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken}
};
@ -74,6 +75,7 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
GithubProject.GitHubAccessToken = (string) dto.ExtraProperties[nameof(GithubProject.GitHubAccessToken)];
GithubProject.GitHubRootUrl = (string) dto.ExtraProperties[nameof(GithubProject.GitHubRootUrl)];
GithubProject.GitHubUserAgent = (string) dto.ExtraProperties[nameof(GithubProject.GitHubUserAgent)];
}
public abstract class EditProjectViewModelBase
@ -116,6 +118,11 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
[DisplayOrder(10001)]
[StringLength(512)]
public string GitHubAccessToken { get; set; }
[DisplayOrder(10002)]
[StringLength(64)]
public string GitHubUserAgent { get; set; }
}
}
}

8
modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Volo.Docs.Projects;
@ -94,15 +95,16 @@ namespace Volo.Docs.Documents
cacheKey,
async () =>
{
Logger.LogInformation($"Not found in the cache. Requesting {documentName} from the store...");
var store = _documentStoreFactory.Create(project.DocumentStoreType);
var document = await store.GetDocument(project, documentName, version);
var document = await store.GetDocumentAsync(project, documentName, version);
Logger.LogInformation($"Document retrieved: {documentName}");
return CreateDocumentWithDetailsDto(project, document);
},
() => new DistributedCacheEntryOptions
{
//TODO: Configurable?
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6),
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2),
SlidingExpiration = TimeSpan.FromMinutes(30)
}
);

2
modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs

@ -68,7 +68,7 @@ namespace Volo.Docs.Projects
protected virtual async Task<List<VersionInfo>> GetVersionsAsync(Project project)
{
var store = _documentStoreFactory.Create(project.DocumentStoreType);
var versions = await store.GetVersions(project);
var versions = await store.GetVersionsAsync(project);
if (!versions.Any())
{

4
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs

@ -7,9 +7,9 @@ namespace Volo.Docs.Documents
{
public interface IDocumentStore : IDomainService
{
Task<Document> GetDocument(Project project, string documentName, string version);
Task<Document> GetDocumentAsync(Project project, string documentName, string version);
Task<List<VersionInfo>> GetVersions(Project project);
Task<List<VersionInfo>> GetVersionsAsync(Project project);
Task<DocumentResource> GetResource(Project project, string resourceName, string version);
}

4
modules/docs/src/Volo.Docs.Domain/Volo/Docs/FileSystem/Documents/FileSystemDocumentStore.cs

@ -14,7 +14,7 @@ namespace Volo.Docs.FileSystem.Documents
{
public const string Type = "FileSystem";
public async Task<Document> GetDocument(Project project, string documentName, string version)
public async Task<Document> GetDocumentAsync(Project project, string documentName, string version)
{
var projectFolder = project.GetFileSystemPath();
var path = Path.Combine(projectFolder, documentName);
@ -41,7 +41,7 @@ namespace Volo.Docs.FileSystem.Documents
};
}
public Task<List<VersionInfo>> GetVersions(Project project)
public Task<List<VersionInfo>> GetVersionsAsync(Project project)
{
return Task.FromResult(new List<VersionInfo>());
}

37
modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs

@ -22,7 +22,7 @@ namespace Volo.Docs.GitHub.Documents
{
public const string Type = "GitHub";
public virtual async Task<Document> GetDocument(Project project, string documentName, string version)
public virtual async Task<Document> GetDocumentAsync(Project project, string documentName, string version)
{
var token = project.GetGitHubAccessTokenOrNull();
var rootUrl = project.GetGitHubUrl(version);
@ -50,13 +50,14 @@ namespace Volo.Docs.GitHub.Documents
Format = project.Format,
LocalDirectory = localDirectory,
FileName = fileName,
Contributors = !isNavigationDocument ? await GetContributors(commitHistoryUrl, token, userAgent): new List<DocumentContributor>(),
Contributors = new List<DocumentContributor>(),
//Contributors = !isNavigationDocument ? await GetContributors(commitHistoryUrl, token, userAgent): new List<DocumentContributor>(),
Version = version,
Content = await DownloadWebContentAsStringAsync(rawDocumentUrl, token, userAgent)
};
}
public async Task<List<VersionInfo>> GetVersions(Project project)
public async Task<List<VersionInfo>> GetVersionsAsync(Project project)
{
List<VersionInfo> versions;
try
@ -148,13 +149,18 @@ namespace Volo.Docs.GitHub.Documents
{
try
{
using (var webClient = new WebClient())
Logger.LogInformation("Downloading content from Github (DownloadWebContentAsStringAsync): " + rawUrl);
using (var webClient = new GithubWebClient())
{
if (!token.IsNullOrWhiteSpace())
{
webClient.Headers.Add("Authorization", "token " + token);
}
webClient.Headers.Add("User-Agent", userAgent ?? "");
//TODO: SET TIMEOUT?
return await webClient.DownloadStringTaskAsync(new Uri(rawUrl));
}
@ -171,7 +177,9 @@ namespace Volo.Docs.GitHub.Documents
{
try
{
using (var webClient = new WebClient())
Logger.LogInformation("Downloading content from Github (DownloadWebContentAsByteArrayAsync): " + rawUrl);
using (var webClient = new GithubWebClient())
{
if (!token.IsNullOrWhiteSpace())
{
@ -219,8 +227,7 @@ namespace Volo.Docs.GitHub.Documents
{
Logger.LogWarning(ex.Message);
}
return contributors;
}
@ -230,5 +237,21 @@ namespace Volo.Docs.GitHub.Documents
.Replace("github.com", "raw.githubusercontent.com")
.ReplaceFirst("/tree/", "/");
}
private class GithubWebClient : WebClient
{
protected override WebRequest GetWebRequest(Uri address)
{
var webRequest = base.GetWebRequest(address);
if (webRequest == null)
{
return null;
}
webRequest.Timeout = 15000;
return webRequest;
}
}
}
}

32
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml

@ -45,7 +45,7 @@
<nav class="navbar navbar-logo">
@if (!Model.Project.Name.IsNullOrWhiteSpace())
{
<a class="navbar-brand w-100" href="@Model.CreateLink(Model.LatestVersionInfo, Model.GetSpecificVersionOrLatest())">
<a class="navbar-brand w-100" href="@Model.CreateVersionLink(Model.LatestVersionInfo, Model.GetSpecificVersionOrLatest())">
<span id="ProjectName">@Model.Project.Name</span><br>
<strong class="display-block">
@L["Documents"]
@ -72,12 +72,21 @@
</div>
<div class="docs-tree-list">
<div class="docs-filter">
<input class="form-control" type="search" placeholder="Filter topics" aria-label="Filter">
<span class="my-2 filter-icon my-sm-0">
<i class="fa fa-filter"></i>
</span>
</div>
@if (Model.ProjectSelectItems.Count > 1)
{
<div class="docs-version">
<div class="version-select">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">@L["Project"]</label>
</div>
<select asp-items="Model.ProjectSelectItems" class="form-control" onchange="window.location.replace(this.value)"></select>
</div>
</div>
</div>
}
@if (Model.VersionSelectItems.Any())
{
@ -88,12 +97,19 @@
<label class="input-group-text">@L["Version"]</label>
</div>
<select asp-items="Model.VersionSelectItems" class="form-control" onchange="window.location.replace(this.value)"></select>
<select asp-items="Model.VersionSelectItems" class="form-control" onchange="if (this.value) { window.location.replace(this.value) }"></select>
</div>
</div>
</div>
}
<div class="docs-filter">
<input class="form-control" type="search" placeholder="Filter topics" aria-label="Filter">
<span class="my-2 filter-icon my-sm-0">
<i class="fa fa-filter"></i>
</span>
</div>
@if (Model.Navigation == null || Model.Navigation.Content.IsNullOrEmpty())
{
<div class="text-muted p-3">

19
modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs

@ -31,6 +31,8 @@ namespace Volo.Docs.Pages.Documents.Project
public List<SelectListItem> VersionSelectItems { get; private set; }
public List<SelectListItem> ProjectSelectItems { get; private set; }
public NavigationWithDetailsDto Navigation { get; private set; }
public VersionInfoViewModel LatestVersionInfo { get; private set; }
@ -52,6 +54,7 @@ namespace Volo.Docs.Pages.Documents.Project
public async Task OnGetAsync()
{
await SetProjectAsync();
await SetProjectsAsync();
await SetVersionAsync();
await SetDocumentAsync();
await SetNavigationAsync();
@ -62,6 +65,18 @@ namespace Volo.Docs.Pages.Documents.Project
Project = await _projectAppService.GetAsync(ProjectName);
}
private async Task SetProjectsAsync()
{
var projects = await _projectAppService.GetListAsync();
ProjectSelectItems = projects.Items.Select(p => new SelectListItem
{
Text = p.Name,
Value = p.Id != Project.Id ? "/documents/" + p.ShortName + "/" + DocsAppConsts.Latest : null,
Selected = p.Id == Project.Id
}).ToList();
}
private async Task SetVersionAsync()
{
//TODO: Needs refactoring
@ -101,7 +116,7 @@ namespace Volo.Docs.Pages.Documents.Project
VersionSelectItems = versions.Select(v => new SelectListItem
{
Text = v.DisplayText,
Value = CreateLink(LatestVersionInfo, v.Version, DocumentName),
Value = CreateVersionLink(LatestVersionInfo, v.Version, DocumentName),
Selected = v.IsSelected
}).ToList();
}
@ -128,7 +143,7 @@ namespace Volo.Docs.Pages.Documents.Project
Navigation.ConvertItems();
}
public string CreateLink(VersionInfoViewModel latestVersion, string version, string documentName = null)
public string CreateVersionLink(VersionInfoViewModel latestVersion, string version, string documentName = null)
{
if (latestVersion == null || latestVersion.Version == version)
{

3
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/AbpIdentityDomainSharedModule.cs

@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Users;

10
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs

@ -7,15 +7,19 @@ using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.SettingManagement;
using Volo.Abp.Settings;
using Volo.Abp.Users;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Identity
{
[DependsOn(typeof(AbpDddDomainModule))]
[DependsOn(typeof(AbpIdentityDomainSharedModule))]
[DependsOn(typeof(AbpUsersDomainModule))]
[DependsOn(
typeof(AbpDddDomainModule),
typeof(AbpIdentityDomainSharedModule),
typeof(AbpUsersDomainModule),
typeof(AbpSettingManagementDomainModule)
)]
public class AbpIdentityDomainModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

1
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs

@ -52,6 +52,7 @@ namespace Volo.Abp.Identity
}
adminUser = new IdentityUser(_guidGenerator.Create(), adminUserName, "admin@abp.io", tenantId);
adminUser.Name = adminUserName;
CheckIdentityErrors(await _userManager.CreateAsync(adminUser, adminUserPassword));
result.CreatedAdminUser = true;

1
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityDataSeeder_Tests.cs

@ -28,6 +28,7 @@ namespace Volo.Abp.Identity
await _identityDataSeeder.SeedAsync("1q2w3E*");
(await _userRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.Normalize("admin"))).ShouldNotBeNull();
(await _userRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.Normalize("admin"))).Name.ShouldBe("admin");
(await _roleRepository.FindByNormalizedNameAsync(_lookupNormalizer.Normalize("admin"))).ShouldNotBeNull();
}
}

1
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj

@ -14,7 +14,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Core\Volo.Abp.Core.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
</ItemGroup>

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

Loading…
Cancel
Save