Merge remote-tracking branch 'upstream/master' into Translate

pull/1200/head
maliming 6 years ago
commit 387e247feb

@ -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;

@ -9,7 +9,7 @@
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<PreserveCompilationContext>true</PreserveCompilationContext>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
<UserSecretsId>c140514f-e488-4c99-8b9a-fabee0f53ce0</UserSecretsId>
</PropertyGroup>

@ -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>

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

@ -24,7 +24,7 @@ This sample aims to demonstrate a simple yet complete microservice solution;
The diagram below shows the system:
![microservice-sample-diagram](../images/microservice-sample-diagram.png)
![microservice-sample-diagram-2](../images/microservice-sample-diagram-2.png)
### Source Code
@ -32,7 +32,7 @@ You can get the source code from [the GitHub repository](https://github.com/abpf
### Status
This sample is still in development, not completed yet.
Initial version of this sample has been completed. Additional improvement are still in development.
## Running the Solution
@ -50,6 +50,20 @@ Running as docker containers is easier since all dependencies are pre-configured
- Open a command line in the `samples/MicroserviceDemo` folder of the repository.
- Pull images from Docker Hub:
```
docker-compose -f docker-compose.yml -f docker-compose.migrations.yml pull
```
- If you want to build images locally you may skip the above step and instead use build command:
```
docker-compose -f docker-compose.yml -f docker-compose.migrations.yml build
```
Building images may take a **long time** depending on your machine.
- Restore SQL Server databases:
```
@ -62,8 +76,6 @@ Running as docker containers is easier since all dependencies are pre-configured
docker-compose up -d
```
At the first run, it will take a **long time** because it will build all docker images.
- Add this line to the end of your `hosts` file:
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

@ -222,9 +222,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.Identi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.IdentityModel", "src\Volo.Abp.IdentityModel\Volo.Abp.IdentityModel.csproj", "{64D99E19-EE25-465A-82E5-17B25F4C4E18}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.Client", "src\Volo.Abp.AspNetCore.Mvc.Client\Volo.Abp.AspNetCore.Mvc.Client.csproj", "{E803DDB8-81EA-454B-9A66-9C2941100B67}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.Client", "src\Volo.Abp.AspNetCore.Mvc.Client\Volo.Abp.AspNetCore.Mvc.Client.csproj", "{E803DDB8-81EA-454B-9A66-9C2941100B67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.Contracts", "src\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj", "{88F6D091-CA16-4B71-9499-8D5B8FA2E712}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.Contracts", "src\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj", "{88F6D091-CA16-4B71-9499-8D5B8FA2E712}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Features", "src\Volo.Abp.Features\Volo.Abp.Features.csproj", "{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -640,6 +642,10 @@ Global
{88F6D091-CA16-4B71-9499-8D5B8FA2E712}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88F6D091-CA16-4B71-9499-8D5B8FA2E712}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88F6D091-CA16-4B71-9499-8D5B8FA2E712}.Release|Any CPU.Build.0 = Release|Any CPU
{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -749,6 +755,7 @@ Global
{64D99E19-EE25-465A-82E5-17B25F4C4E18} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{E803DDB8-81EA-454B-9A66-9C2941100B67} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{88F6D091-CA16-4B71-9499-8D5B8FA2E712} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{01E3D389-8872-4EB1-9D3D-13B6ED54DE0E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

@ -14,19 +14,17 @@ namespace Volo.Abp.AspNetCore.Mvc.Client
ConfigurationClient = configurationClient;
}
public async Task<PermissionGrantInfo> CheckAsync(string name)
public async Task<bool> IsGrantedAsync(string name)
{
var configuration = await ConfigurationClient.GetAsync();
return new PermissionGrantInfo(
name,
configuration.Auth.GrantedPolicies.ContainsKey(name)
);
return configuration.Auth.GrantedPolicies.ContainsKey(name);
}
public Task<PermissionGrantInfo> CheckAsync(ClaimsPrincipal claimsPrincipal, string name)
public Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
return CheckAsync(name);
/* This provider always works for the current principal. */
return IsGrantedAsync(name);
}
}
}

@ -23,7 +23,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
public AbpFormControlSize Size { get; set; } = AbpFormControlSize.Default;
[HtmlAttributeNotBound]
[HtmlAttributeName("required-symbol")]
public bool DisplayRequiredSymbol { get; set; } = true;
[HtmlAttributeNotBound]

@ -18,7 +18,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
[HtmlAttributeName("info")]
public string InfoText { get; set; }
[HtmlAttributeNotBound]
[HtmlAttributeName("required-symbol")]
public bool DisplayRequiredSymbol { get; set; } = true;
[HtmlAttributeNotBound]

@ -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>
}

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

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.FlagIconCss
{
public class FlagIconCssStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/flag-icon-css/css/flag-icon.min.css");
}
}
}

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

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

@ -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>
}
}
}

@ -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>
}

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

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

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

@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp.Threading;
namespace Volo.Abp.Authorization.Permissions
{
@ -11,14 +12,14 @@ namespace Volo.Abp.Authorization.Permissions
/// </summary>
public class AlwaysAllowPermissionChecker : IPermissionChecker
{
public Task<PermissionGrantInfo> CheckAsync(string name)
public Task<bool> IsGrantedAsync(string name)
{
return Task.FromResult(new PermissionGrantInfo(name, true, "AlwaysAllow"));
return TaskCache.TrueResult;
}
public Task<PermissionGrantInfo> CheckAsync(ClaimsPrincipal claimsPrincipal, string name)
public Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
return Task.FromResult(new PermissionGrantInfo(name, true, "AlwaysAllow"));
return TaskCache.TrueResult;
}
}
}

@ -15,21 +15,18 @@ namespace Volo.Abp.Authorization.Permissions
}
public override async Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context)
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value;
if (clientId == null)
{
return PermissionValueProviderGrantInfo.NonGranted;
return PermissionGrantResult.Undefined;
}
if (await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, clientId))
{
return new PermissionValueProviderGrantInfo(true, clientId);
}
return PermissionValueProviderGrantInfo.NonGranted;
return await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, clientId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
}
}

@ -6,8 +6,8 @@ namespace Volo.Abp.Authorization.Permissions
{
public interface IPermissionChecker
{
Task<PermissionGrantInfo> CheckAsync([NotNull]string name);
Task<bool> IsGrantedAsync([NotNull]string name);
Task<PermissionGrantInfo> CheckAsync([CanBeNull] ClaimsPrincipal claimsPrincipal, [NotNull]string name);
Task<bool> IsGrantedAsync([CanBeNull] ClaimsPrincipal claimsPrincipal, [NotNull]string name);
}
}

@ -7,6 +7,7 @@ namespace Volo.Abp.Authorization.Permissions
{
string Name { get; }
Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context);
//TODO: Rename to GetResult? (CheckAsync throws exception by naming convention)
Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context);
}
}

@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.Authorization.Permissions
{
@ -16,7 +17,7 @@ namespace Volo.Abp.Authorization.Permissions
public Task<bool> IsGrantedAsync(string name, string providerName, string providerKey)
{
return Task.FromResult(false);
return TaskCache.FalseResult;
}
}
}

@ -41,12 +41,12 @@ namespace Volo.Abp.Authorization.Permissions
);
}
public virtual Task<PermissionGrantInfo> CheckAsync(string name)
public virtual Task<bool> IsGrantedAsync(string name)
{
return CheckAsync(PrincipalAccessor.Principal, name);
return IsGrantedAsync(PrincipalAccessor.Principal, name);
}
public virtual async Task<PermissionGrantInfo> CheckAsync(ClaimsPrincipal claimsPrincipal, string name)
public virtual async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
Check.NotNull(name, nameof(name));
@ -55,6 +55,8 @@ namespace Volo.Abp.Authorization.Permissions
claimsPrincipal
);
var isGranted = false;
foreach (var provider in ValueProviders)
{
if (context.Permission.Providers.Any() &&
@ -64,13 +66,18 @@ namespace Volo.Abp.Authorization.Permissions
}
var result = await provider.CheckAsync(context);
if (result.IsGranted)
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return new PermissionGrantInfo(context.Permission.Name, true, provider.Name, result.ProviderKey);
return false;
}
}
return new PermissionGrantInfo(context.Permission.Name, false);
return isGranted;
}
}
}

@ -1,20 +0,0 @@
using System.Security.Claims;
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions
{
public static class PermissionCheckerExtensions
{
public static async Task<bool> IsGrantedAsync(this IPermissionChecker permissionChecker, string name)
{
return (await permissionChecker.CheckAsync(name)).IsGranted;
}
public static async Task<bool> IsGrantedAsync(this IPermissionChecker permissionChecker, ClaimsPrincipal principal, string name)
{
return (await permissionChecker.CheckAsync(principal, name)).IsGranted;
}
//TODO: Add sync extensions
}
}

@ -22,7 +22,7 @@ namespace Volo.Abp.Authorization.Permissions
/// A list of allowed providers to get/set value of this permission.
/// An empty list indicates that all providers are allowed.
/// </summary>
public List<string> Providers { get; }
public List<string> Providers { get; } //TODO: Rename to AllowedProviders?
public ILocalizableString DisplayName
{
@ -53,7 +53,9 @@ namespace Volo.Abp.Authorization.Permissions
set => Properties[name] = value;
}
protected internal PermissionDefinition([NotNull] string name, ILocalizableString displayName = null)
protected internal PermissionDefinition(
[NotNull] string name,
ILocalizableString displayName = null)
{
Name = Check.NotNull(name, nameof(name));
DisplayName = displayName ?? new FixedLocalizableString(name);
@ -63,7 +65,9 @@ namespace Volo.Abp.Authorization.Permissions
_children = new List<PermissionDefinition>();
}
public virtual PermissionDefinition AddChild([NotNull] string name, ILocalizableString displayName = null)
public virtual PermissionDefinition AddChild(
[NotNull] string name,
ILocalizableString displayName = null)
{
var child = new PermissionDefinition(name, displayName)
{

@ -0,0 +1,9 @@
namespace Volo.Abp.Authorization.Permissions
{
public enum PermissionGrantResult
{
Undefined,
Granted,
Prohibited
}
}

@ -37,7 +37,9 @@ namespace Volo.Abp.Authorization.Permissions
set => Properties[name] = value;
}
protected internal PermissionGroupDefinition(string name, ILocalizableString displayName = null)
protected internal PermissionGroupDefinition(
string name,
ILocalizableString displayName = null)
{
Name = name;
DisplayName = displayName ?? new FixedLocalizableString(Name);
@ -46,7 +48,9 @@ namespace Volo.Abp.Authorization.Permissions
_permissions = new List<PermissionDefinition>();
}
public virtual PermissionDefinition AddPermission(string name, ILocalizableString displayName = null)
public virtual PermissionDefinition AddPermission(
string name,
ILocalizableString displayName = null)
{
var permission = new PermissionDefinition(name, displayName);

@ -11,7 +11,9 @@ namespace Volo.Abp.Authorization.Permissions
[CanBeNull]
public ClaimsPrincipal Principal { get; }
public PermissionValueCheckContext([NotNull] PermissionDefinition permission, [CanBeNull] ClaimsPrincipal principal)
public PermissionValueCheckContext(
[NotNull] PermissionDefinition permission,
[CanBeNull] ClaimsPrincipal principal)
{
Check.NotNull(permission, nameof(permission));

@ -13,6 +13,6 @@ namespace Volo.Abp.Authorization.Permissions
PermissionStore = permissionStore;
}
public abstract Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context);
public abstract Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context);
}
}

@ -16,23 +16,23 @@ namespace Volo.Abp.Authorization.Permissions
}
public override async Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context)
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
if (roles == null || !roles.Any())
{
return PermissionValueProviderGrantInfo.NonGranted;
return PermissionGrantResult.Undefined;
}
foreach (var role in roles)
{
if (await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, role))
{
return new PermissionValueProviderGrantInfo(true, role);
return PermissionGrantResult.Granted;
}
}
return PermissionValueProviderGrantInfo.NonGranted;
return PermissionGrantResult.Undefined;
}
}
}

@ -15,21 +15,18 @@ namespace Volo.Abp.Authorization.Permissions
}
public override async Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context)
public override async Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
{
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
return PermissionValueProviderGrantInfo.NonGranted;
return PermissionGrantResult.Undefined;
}
if (await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, userId))
{
return new PermissionValueProviderGrantInfo(true, userId);
}
return PermissionValueProviderGrantInfo.NonGranted;
return await PermissionStore.IsGrantedAsync(context.Permission.Name, Name, userId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
}
}

@ -8,17 +8,11 @@ namespace Volo.Abp
[Serializable]
public class NameValue : NameValue<string>
{
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue()
{
}
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue(string name, string value)
{
Name = name;
@ -42,17 +36,11 @@ namespace Volo.Abp
/// </summary>
public T Value { get; set; }
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue()
{
}
/// <summary>
/// Creates a new <see cref="NameValue"/>.
/// </summary>
public NameValue(string name, T value)
{
Name = name;

@ -0,0 +1,16 @@
using System.Threading.Tasks;
namespace Volo.Abp.Threading
{
public static class TaskCache
{
public static Task<bool> TrueResult { get; }
public static Task<bool> FalseResult { get; }
static TaskCache()
{
TrueResult = Task.FromResult(true);
FalseResult = Task.FromResult(false);
}
}
}

@ -39,5 +39,12 @@ namespace Volo.Abp.Data
source.ExtraProperties[name] = value;
return source;
}
public static TSource RemoveProperty<TSource>(this TSource source, string name)
where TSource : IHasExtraProperties
{
source.ExtraProperties.Remove(name);
return source;
}
}
}

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

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.Features</AssemblyName>
<PackageId>Volo.Abp.Features</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Localization.Abstractions\Volo.Abp.Localization.Abstractions.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy.Abstractions\Volo.Abp.MultiTenancy.Abstractions.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,18 @@
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Features
{
[DependsOn(
typeof(AbpLocalizationAbstractionsModule),
typeof(AbpMultiTenancyAbstractionsModule)
)]
public class AbpFeaturesModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
}
}
}

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Features
{
public class FeatureChecker : IFeatureChecker, ITransientDependency
{
protected IFeatureDefinitionManager FeatureDefinitionManager { get; }
protected Lazy<List<IFeatureValueProvider>> Providers { get; }
protected FeatureOptions Options { get; }
public FeatureChecker(
IOptions<FeatureOptions> options,
IServiceProvider serviceProvider,
IFeatureDefinitionManager featureDefinitionManager)
{
FeatureDefinitionManager = featureDefinitionManager;
Options = options.Value;
Providers = new Lazy<List<IFeatureValueProvider>>(
() => Options
.ValueProviders
.Select(type => serviceProvider.GetRequiredService(type) as IFeatureValueProvider)
.ToList(),
true
);
}
public virtual async Task<string> GetOrNullAsync(string name)
{
var featureDefinition = FeatureDefinitionManager.Get(name);
var providers = Enumerable
.Reverse(Providers.Value);
if (featureDefinition.AllowedProviders.Any())
{
providers = providers.Where(p => featureDefinition.AllowedProviders.Contains(p.Name));
}
return await GetOrNullValueFromProvidersAsync(providers, featureDefinition);
}
public async Task<bool> IsEnabledAsync(string name)
{
var value = await GetOrNullAsync(name);
if (value == null)
{
return false;
}
try
{
return bool.Parse(value);
}
catch (Exception ex)
{
throw new AbpException(
$"The value '{value}' for the feature '{name}' should be a boolean, but was not!",
ex
);
}
}
protected virtual async Task<string> GetOrNullValueFromProvidersAsync(
IEnumerable<IFeatureValueProvider> providers,
FeatureDefinition feature)
{
foreach (var provider in providers)
{
var value = await provider.GetOrNullAsync(feature);
if (value != null)
{
return value;
}
}
return null;
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp.Threading;
namespace Volo.Abp.Features
{
public static class FeatureCheckerExtensions
{
public static async Task<T> GetAsync<T>([NotNull] this IFeatureChecker featureChecker, [NotNull] string name, T defaultValue = default)
where T : struct
{
Check.NotNull(featureChecker, nameof(featureChecker));
Check.NotNull(name, nameof(name));
var value = await featureChecker.GetOrNullAsync(name);
return value?.To<T>() ?? defaultValue;
}
public static string GetOrNull([NotNull] this IFeatureChecker featureChecker, [NotNull] string name)
{
Check.NotNull(featureChecker, nameof(featureChecker));
return AsyncHelper.RunSync(() => featureChecker.GetOrNullAsync(name));
}
public static T Get<T>([NotNull] this IFeatureChecker featureChecker, [NotNull] string name, T defaultValue = default)
where T : struct
{
return AsyncHelper.RunSync(() => featureChecker.GetAsync(name, defaultValue));
}
public static bool IsEnabled([NotNull] this IFeatureChecker featureChecker, [NotNull] string name)
{
return AsyncHelper.RunSync(() => featureChecker.IsEnabledAsync(name));
}
}
}

@ -0,0 +1,150 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using JetBrains.Annotations;
using Volo.Abp.Localization;
namespace Volo.Abp.Features
{
public class FeatureDefinition
{
/// <summary>
/// Unique name of the feature.
/// </summary>
[NotNull]
public string Name { get; }
[NotNull]
public ILocalizableString DisplayName
{
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName;
[CanBeNull]
public ILocalizableString Description { get; set; }
/// <summary>
/// Parent of this feature, if one exists.
/// If set, this feature can be enabled only if the parent is enabled.
/// </summary>
public FeatureDefinition Parent { get; private set; }
/// <summary>
/// List of child features.
/// </summary>
public IReadOnlyList<FeatureDefinition> Children => _children.ToImmutableList();
private readonly List<FeatureDefinition> _children;
/// <summary>
/// Default value of the feature.
/// </summary>
[CanBeNull]
public string DefaultValue { get; set; }
/// <summary>
/// Can clients see this feature and it's value.
/// Default: true.
/// </summary>
public bool IsVisibleToClients { get; set; }
/// <summary>
/// A list of allowed providers to get/set value of this feature.
/// An empty list indicates that all providers are allowed.
/// </summary>
public List<string> AllowedProviders { get; }
/// <summary>
/// Can be used to get/set custom properties for this feature.
/// </summary>
[NotNull]
public Dictionary<string, object> Properties { get; }
//TODO: Implement input type like old ABP!
public FeatureDefinition(
string name,
string defaultValue = null,
ILocalizableString displayName = null,
ILocalizableString description = null,
bool isVisibleToClients = true)
{
Name = name;
DefaultValue = defaultValue;
IsVisibleToClients = isVisibleToClients;
DisplayName = displayName ?? new FixedLocalizableString(name);
Description = description;
Properties = new Dictionary<string, object>();
AllowedProviders = new List<string>();
_children = new List<FeatureDefinition>();
}
/// <summary>
/// Sets a property in the <see cref="Properties"/> dictionary.
/// This is a shortcut for nested calls on this object.
/// </summary>
public virtual FeatureDefinition WithProperty(string key, object value)
{
Properties[key] = value;
return this;
}
/// <summary>
/// Sets a property in the <see cref="Properties"/> dictionary.
/// This is a shortcut for nested calls on this object.
/// </summary>
public virtual FeatureDefinition WithProviders(params string[] providers)
{
if (!providers.IsNullOrEmpty())
{
AllowedProviders.AddRange(providers);
}
return this;
}
/// <summary>
/// Adds a child feature.
/// </summary>
/// <returns>Returns a newly created child feature</returns>
public FeatureDefinition CreateChild(
string name,
string defaultValue = null,
ILocalizableString displayName = null,
ILocalizableString description = null,
bool isVisibleToClients = true)
{
var feature = new FeatureDefinition(
name,
defaultValue,
displayName,
description,
isVisibleToClients)
{
Parent = this
};
_children.Add(feature);
return feature;
}
public void RemoveChild(string name)
{
var featureToRemove = _children.FirstOrDefault(f => f.Name == name);
if (featureToRemove == null)
{
throw new AbpException($"Could not find a feature named '{name}' in the Children of this feature '{Name}'.");
}
featureToRemove.Parent = null;
_children.Remove(featureToRemove);
}
public override string ToString()
{
return $"[{nameof(FeatureDefinition)}: {Name}]";
}
}
}

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Volo.Abp.Features
{
public class FeatureDefinitionContext : IFeatureDefinitionContext
{
protected Dictionary<string, FeatureDefinition> Features { get; }
public FeatureDefinitionContext(Dictionary<string, FeatureDefinition> features)
{
Features = features;
}
public virtual FeatureDefinition GetOrNull(string name)
{
return Features.GetOrDefault(name);
}
public virtual IReadOnlyList<FeatureDefinition> GetAll()
{
return Features.Values.ToImmutableList();
}
public virtual void Add(params FeatureDefinition[] definitions)
{
if (definitions.IsNullOrEmpty())
{
return;
}
foreach (var definition in definitions)
{
Features[definition.Name] = definition;
}
}
}
}

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Features
{
public class FeatureDefinitionManager : IFeatureDefinitionManager, ISingletonDependency
{
protected Lazy<List<IFeatureDefinitionProvider>> Providers { get; }
protected Lazy<IDictionary<string, FeatureDefinition>> FeatureDefinitions { get; }
protected FeatureOptions Options { get; }
private readonly IServiceProvider _serviceProvider;
public FeatureDefinitionManager(
IOptions<FeatureOptions> options,
IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
Options = options.Value;
Providers = new Lazy<List<IFeatureDefinitionProvider>>(CreateFeatureProviders, true);
FeatureDefinitions = new Lazy<IDictionary<string, FeatureDefinition>>(CreateFeatureDefinitions, true);
}
public virtual FeatureDefinition Get(string name)
{
Check.NotNull(name, nameof(name));
var feature = GetOrNull(name);
if (feature == null)
{
throw new AbpException("Undefined feature: " + name);
}
return feature;
}
public virtual IReadOnlyList<FeatureDefinition> GetAll()
{
return FeatureDefinitions.Value.Values.ToImmutableList();
}
public virtual FeatureDefinition GetOrNull(string name)
{
return FeatureDefinitions.Value.GetOrDefault(name);
}
protected virtual List<IFeatureDefinitionProvider> CreateFeatureProviders()
{
return Options
.DefinitionProviders
.Select(p => _serviceProvider.GetRequiredService(p) as IFeatureDefinitionProvider)
.ToList();
}
protected virtual IDictionary<string, FeatureDefinition> CreateFeatureDefinitions()
{
var features = new Dictionary<string, FeatureDefinition>();
foreach (var provider in Providers.Value)
{
provider.Define(new FeatureDefinitionContext(features));
}
return features;
}
}
}

@ -0,0 +1,9 @@
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Features
{
public abstract class FeatureDefinitionProvider : IFeatureDefinitionProvider, ISingletonDependency
{
public abstract void Define(IFeatureDefinitionContext context);
}
}

@ -0,0 +1,17 @@
using Volo.Abp.Collections;
namespace Volo.Abp.Features
{
public class FeatureOptions
{
public ITypeList<IFeatureDefinitionProvider> DefinitionProviders { get; }
public ITypeList<IFeatureValueProvider> ValueProviders { get; }
public FeatureOptions()
{
DefinitionProviders = new TypeList<IFeatureDefinitionProvider>();
ValueProviders = new TypeList<IFeatureValueProvider>();
}
}
}

@ -0,0 +1,19 @@
using System;
namespace Volo.Abp.Features
{
[Serializable]
public class FeatureValue : NameValue
{
public FeatureValue()
{
}
public FeatureValue(string name, string value)
{
Name = name;
Value = value;
}
}
}

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Features
{
public abstract class FeatureValueProvider : IFeatureValueProvider, ISingletonDependency
{
public abstract string Name { get; }
protected IFeatureStore FeatureStore { get; }
protected FeatureValueProvider(IFeatureStore featureStore)
{
FeatureStore = featureStore;
}
public abstract Task<string> GetOrNullAsync(FeatureDefinition feature);
}
}

@ -0,0 +1,12 @@
using JetBrains.Annotations;
using System.Threading.Tasks;
namespace Volo.Abp.Features
{
public interface IFeatureChecker
{
Task<string> GetOrNullAsync([NotNull] string name);
Task<bool> IsEnabledAsync(string name);
}
}

@ -0,0 +1,9 @@
namespace Volo.Abp.Features
{
public interface IFeatureDefinitionContext
{
FeatureDefinition GetOrNull(string name);
void Add(params FeatureDefinition[] definitions);
}
}

@ -0,0 +1,15 @@
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.Features
{
public interface IFeatureDefinitionManager
{
[NotNull]
FeatureDefinition Get([NotNull] string name);
IReadOnlyList<FeatureDefinition> GetAll();
FeatureDefinition GetOrNull(string name);
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.Features
{
public interface IFeatureDefinitionProvider
{
void Define(IFeatureDefinitionContext context);
}
}

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.Features
{
public interface IFeatureStore
{
Task<string> GetOrNullAsync(
[NotNull] string name,
[CanBeNull] string providerName,
[CanBeNull] string providerKey
);
}
}

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.Features
{
public interface IFeatureValueProvider
{
string Name { get; }
Task<string> GetOrNullAsync([NotNull] FeatureDefinition feature);
}
}

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Features
{
public class NullFeatureStore : IFeatureStore, ISingletonDependency
{
public ILogger<NullFeatureStore> Logger { get; set; }
public NullFeatureStore()
{
Logger = NullLogger<NullFeatureStore>.Instance;
}
public Task<string> GetOrNullAsync(string name, string providerName, string providerKey)
{
return Task.FromResult((string) null);
}
}
}

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Features
{
public class TenantFeatureValueProvider : FeatureValueProvider
{
public const string ProviderName = "Tenant";
public override string Name => ProviderName;
protected ICurrentTenant CurrentTenant { get; }
public TenantFeatureValueProvider(IFeatureStore featureStore, ICurrentTenant currentTenant)
: base(featureStore)
{
CurrentTenant = currentTenant;
}
public override async Task<string> GetOrNullAsync(FeatureDefinition feature)
{
return await FeatureStore.GetOrNullAsync(feature.Name, Name, CurrentTenant.Id?.ToString());
}
}
}

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

@ -1,10 +1,12 @@
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.Security;
namespace Volo.Abp.MultiTenancy
{
[DependsOn(
typeof(AbpDataModule)
typeof(AbpDataModule),
typeof(AbpSecurityModule)
)]
public class AbpMultiTenancyAbstractionsModule : AbpModule //TODO: Rename to AbpMultiTenancyModule?
{

@ -2,7 +2,6 @@
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security;
using Volo.Abp.Users;
namespace Volo.Abp.Settings
{

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
@ -19,21 +18,5 @@ namespace Volo.Abp.Settings
{
return Task.FromResult((string) null);
}
public Task SetAsync(string name, string value, string providerName, string providerKey)
{
Logger.LogWarning($"Setting the value for {name} is not possible because current setting store is {nameof(NullSettingStore)}");
return Task.CompletedTask;
}
public Task<List<SettingValue>> GetListAsync(string providerName, string providerKey)
{
return Task.FromResult(new List<SettingValue>());
}
public Task DeleteAsync(string name, string providerName, string providerKey)
{
return Task.CompletedTask;
}
}
}

@ -40,7 +40,7 @@ namespace Volo.Abp.Settings
/// A list of allowed providers to get/set value of this setting.
/// An empty list indicates that all providers are allowed.
/// </summary>
public List<string> Providers { get; }
public List<string> Providers { get; } //TODO: Rename to AllowedProviders
/// <summary>
/// Is this setting inherited from parent scopes.

@ -29,7 +29,7 @@ namespace Volo.Abp.Settings
Providers = new Lazy<List<ISettingValueProvider>>(
() => Options
.ValueProviders
.Select(c => serviceProvider.GetRequiredService(c) as ISettingValueProvider)
.Select(type => serviceProvider.GetRequiredService(type) as ISettingValueProvider)
.ToList(),
true
);

@ -13,7 +13,7 @@
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
</PropertyGroup>
<ItemGroup>

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

@ -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]

@ -0,0 +1,33 @@
using System;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.TestApp.Domain;
using Xunit;
namespace Volo.Abp.TestApp.Testing
{
public class HasExtraPropertiesExtensions_Tests
{
[Fact]
public void Basic_Tests()
{
var city = new City(Guid.NewGuid(), "Adana");
city.HasProperty("UnknownProperty").ShouldBeFalse();
city.GetProperty("UnknownProperty").ShouldBeNull();
city.GetProperty<int>("UnknownProperty").ShouldBe(0);
city.SetProperty("IsHot", true);
city.HasProperty("IsHot").ShouldBeTrue();
city.GetProperty<bool>("IsHot").ShouldBeTrue();
city.SetProperty("IsHot", false);
city.HasProperty("IsHot").ShouldBeTrue();
city.GetProperty<bool>("IsHot").ShouldBeFalse();
city.RemoveProperty("IsHot");
city.HasProperty("IsHot").ShouldBeFalse();
city.GetProperty<bool>("IsHot").ShouldBeFalse();
}
}
}

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

@ -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>

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

@ -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>

@ -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">

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

@ -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

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

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

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

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

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

@ -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" />

@ -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,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);

@ -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
{

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

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

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

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

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

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

@ -30,29 +30,46 @@ 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 bool Update(IUserData user)
public virtual bool Update(IUserData user)
{
if (UserName == user.UserName &&
Name == user.Name &&
Surname == user.Surname &&
Email == user.Email &&
EmailConfirmed == user.EmailConfirmed &&
PhoneNumber == user.PhoneNumber &&
PhoneNumberConfirmed == user.PhoneNumberConfirmed)
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;
}
protected virtual void UpdateInternal(IUserData user)
{
Email = user.Email;
Name = user.Name;
Surname = user.Surname;
@ -60,8 +77,6 @@ namespace Volo.Blogging.Users
PhoneNumber = user.PhoneNumber;
PhoneNumberConfirmed = user.PhoneNumberConfirmed;
UserName = user.UserName;
return true;
}
}
}

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

@ -32,8 +32,10 @@ namespace Volo.Blogging.Users
}
}
user.Update(eventData.Entity);
await UserRepository.UpdateAsync(user);
if (user.Update(eventData.Entity))
{
await UserRepository.UpdateAsync(user);
}
}
}
}

@ -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();

@ -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();

@ -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 />

@ -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();

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

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

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

Loading…
Cancel
Save