diff --git a/samples/MicroserviceDemo/.dockerignore b/.dockerignore similarity index 91% rename from samples/MicroserviceDemo/.dockerignore rename to .dockerignore index 3a78c9d4c3..0380893c80 100644 --- a/samples/MicroserviceDemo/.dockerignore +++ b/.dockerignore @@ -10,4 +10,5 @@ docker-compose* */obj README.md LICENSE -.vscode \ No newline at end of file +.vscode +.vs \ No newline at end of file diff --git a/abp_io/src/Volo.AbpWebSite.Web/AbpWebSiteWebModule.cs b/abp_io/src/Volo.AbpWebSite.Web/AbpWebSiteWebModule.cs index bbbce26cee..e1c30e0a26 100644 --- a/abp_io/src/Volo.AbpWebSite.Web/AbpWebSiteWebModule.cs +++ b/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(options => + Configure(options => + { + options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files"); + options.FileUploadUrlRoot = "/files/"; + }); + } + + private void ConfigureLanguages() + { + Configure(options => { options.Languages.Add(new LanguageInfo("en-US", "en-US", "English")); }); } - private static void ConfigureBundles(IServiceCollection services) + private void ConfigureBundles() { - services.Configure(options => + Configure(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(options => + Configure(options => { options.ConnectionStrings.Default = configuration.GetConnectionString("Default"); }); - services.Configure(options => + Configure(options => { options.UseSqlServer(); }); } - private static void ConfigureVirtualFileSystem(IServiceCollection services, IHostingEnvironment hostingEnvironment) + private void ConfigureVirtualFileSystem(IHostingEnvironment hostingEnvironment) { if (hostingEnvironment.IsDevelopment()) { - services.Configure(options => + Configure(options => { options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}..{0}framework{0}src{0}Volo.Abp.UI", Path.DirectorySeparatorChar))); options.FileSets.ReplaceEmbeddedByPhysical(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(options => + Configure(options => { options.Themes.Add(); 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()) diff --git a/abp_io/src/Volo.AbpWebSite.Web/CorrelationIdLogEventEnricher.cs b/abp_io/src/Volo.AbpWebSite.Web/CorrelationIdLogEventEnricher.cs new file mode 100644 index 0000000000..ee7b0bd248 --- /dev/null +++ b/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()) + ) + ); + } + } +} \ No newline at end of file diff --git a/abp_io/src/Volo.AbpWebSite.Web/Program.cs b/abp_io/src/Volo.AbpWebSite.Web/Program.cs index 0ae7a19900..7b458c66de 100644 --- a/abp_io/src/Volo.AbpWebSite.Web/Program.cs +++ b/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() + .UseSerilog() .Build(); } } diff --git a/abp_io/src/Volo.AbpWebSite.Web/Startup.cs b/abp_io/src/Volo.AbpWebSite.Web/Startup.cs index efbfda982a..ce76e2aa8c 100644 --- a/abp_io/src/Volo.AbpWebSite.Web/Startup.cs +++ b/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(); diff --git a/abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj b/abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj index 8fbbbfd711..f73fb7942f 100644 --- a/abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj +++ b/abp_io/src/Volo.AbpWebSite.Web/Volo.AbpWebSite.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/build/build.ps1 b/build/build.ps1 deleted file mode 100644 index c594ce2280..0000000000 --- a/build/build.ps1 +++ /dev/null @@ -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 \ No newline at end of file diff --git a/common.props b/common.props index 040172c6d4..380276a7e1 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 0.13.0 + 0.14.0 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 314feb5709..0000000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/docker/down.ps1 b/docker/down.ps1 deleted file mode 100644 index 508a7c4d74..0000000000 --- a/docker/down.ps1 +++ /dev/null @@ -1 +0,0 @@ -docker-compose down -v --rmi local \ No newline at end of file diff --git a/docker/haproxy.cfg b/docker/haproxy.cfg deleted file mode 100644 index 276ad980c8..0000000000 --- a/docker/haproxy.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docker/up.ps1 b/docker/up.ps1 deleted file mode 100644 index 49ee1ee616..0000000000 --- a/docker/up.ps1 +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md new file mode 100644 index 0000000000..f11a3f19e3 --- /dev/null +++ b/docs/en/Audit-Logging.md @@ -0,0 +1,3 @@ +# Audit Logging + +TODO \ No newline at end of file diff --git a/docs/en/Blog-Posts/2019-02-22/Post.md b/docs/en/Blog-Posts/2019-02-22/Post.md new file mode 100644 index 0000000000..35f8219014 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/docs/en/Blog-Posts/2019-02-22/scott-and-jon.png b/docs/en/Blog-Posts/2019-02-22/scott-and-jon.png new file mode 100644 index 0000000000..79ad21aee7 Binary files /dev/null and b/docs/en/Blog-Posts/2019-02-22/scott-and-jon.png differ diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index fe1ff3450b..8d3c898787 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -152,6 +152,27 @@ services.AddApplication(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() + .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). + diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 747c733683..fcedcd0f89 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -19,7 +19,8 @@ This sample aims to demonstrate a simple yet complete microservice solution; * 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 [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications. +* 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/)). The diagram below shows the system: @@ -33,8 +34,1409 @@ You can get the source code from [the GitHub repository](https://github.com/abpf This sample is still in development, not completed yet. +## Running the Solution + +You can either run from the **source code** or from the pre-configured **docker-compose** file. + +### Using the Docker Containers + +#### Pre Requirements + +Running as docker containers is easier since all dependencies are pre-configured. You only need to install the [latest docker](https://docs.docker.com/compose/install/). + +#### Running Containers + +- Clone or download the [ABP repository](https://github.com/abpframework/abp). + +- Open a command line in the `samples/MicroserviceDemo` folder of the repository. + +- Restore SQL Server databases: + + ``` + docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database + ``` + +- Start the containers: + + ``` + 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: + + ``` + 127.0.0.1 auth-server + ``` + + hosts file is located inside the `C:\Windows\System32\Drivers\etc\hosts` folder on Windows and `/etc/hosts` for Linux/MacOS. + +#### Run the Applications + +There are a few applications running in the containers you may want to explore: + +* Backend Admin Application (BackendAdminApp.Host): `http://localhost:51512` + *(Used to manage users & products in the system)* +* Public Web Site (PublicWebsite.Host): `http://localhost:51513` + *(Used to list products and run/manage the blog module)* +* Authentication Server (AuthServer.Host): `http://auth-server:51511/` + *(Used as a single sign on and authentication server built with IdentityServer4)* +* Kibana UI: `http://localhost:51510` + *(Use to show/trace logs written by all services/applications/gateways)* + +### Running From the Source Code + +#### Pre Requirements + +To be able to run the solution from source code, following tools should be installed and running on your computer: + +* [SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 2015+ (can be [express edition](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express)) +* [Redis](https://redis.io/download) 5.0+ +* [RabbitMQ](https://www.rabbitmq.com/install-windows.html) 3.7.11+ +* [MongoDB](https://www.mongodb.com/download-center) 4.0+ +* [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ +* [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to show logs) + +#### Open & Build the Visual Studio Solution + +* Open the `samples\MicroserviceDemo\MicroserviceDemo.sln` in Visual Studio 2017 (15.9.0+). +* Run `dotnet restore` from the command line inside the `samples\MicroserviceDemo` folder. +* Build the solution in Visual Studio. + +#### Restore Databases + +Open `MsDemo_Identity.zip` and `MsDemo_ProductManagement.zip` inside the `samples\MicroserviceDemo\databases` folder and restore to the SQL Server. + +> Notice that: These databases have EF Core migrations in the solution, however they don't have seed data, especially required for IdentityServer4 configuration. So, restoring the databases is much more easier. + +#### Run Projects + +Run the projects with the following order (right click to each project, set as startup project an press Ctrl+F5 to run without debug): + +* AuthServer.Host +* IdentityService.Host +* BloggingService.Host +* ProductService.Host +* InternalGateway.Host +* BackendAdminAppGateway.Host +* PublicWebSiteGateway.Host +* BackendAdminApp.Host +* PublicWebSite.Host + +## A Brief Overview of the Solution + +The Visual Studio solution consists of multiple projects each have different roles in the system: + +![microservice-sample-solution](../images/microservice-sample-solution.png) + +### Applications + +These are the actual applications those have user interfaces to interact to the users and use the system. + +- **AuthServer.Host**: Host the IdentityServer4 to provide an authentication service to other services and applications. It is a single-sign server and contains the login page. +- **BackendAdminApp.Host**: This is a backend admin application that host UI for Identity and Product management modules. +- **PubicWebSite.Host**: As public web site that contains a simple product list page and blog module UI. +- **ConsoleClientDemo**: A simple console application to demonstrate the usage of services from a C# application. + +### Gateways / BFFs (Backend for Frontend) + +Gateways are used to provide a single entry point to the applications. It can also used for rate limiting, load balancing... etc. Used the [Ocelot](https://github.com/ThreeMammals/Ocelot) library. + +* **BackendAdminAppGateway.Host**: Used by the BackendAdminApp.Host application as backend. +* **PublicWebSiteGateway.Host**: Used by the PublicWebSite.Host application as backend. +* **InternalGateway.Host**: Used for inter-service communication (the communication between microservices). + +### Microservices + +Microservices have no UI, but exposes some REST APIs. + +- **IdentityService.Host**: Host the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module's API. +- **BloggingService.Host**: Host the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module's API. +- **ProductService.Host**: Hosts the Product module (that is inside the solution) which is used to manage products. It also contains the EF Core migrations to create/update the Product Management database schema. + +### Modules + +* **Product**: A layered module that is developed with the [module development best practices](../Best-Practices/Index.md). It can be embedded into a monolithic application or can be hosted as a microservice by separately deploying API and UI (as done in this demo solution). + +### Databases + +This solution is using multiple databases: + +* **MsDemo_Identity**: An SQL database. Used **SQL Server** by default, but can be any DBMS supported by the EF Core. Shared by AuthServer and IdentityService. Also audit logs, permissions and settings are stored in this database (while they could easily have their own databases, shared the same database to keep it simple). +* **MsDemo_ProductManagement**: An SQL database. Again, used **SQL Server** by default, but can be any DBMS supported by the EF Core. Used by the ProductService as a dedicated database. +* **MsDemo_Blogging**: A **MongoDB** database. Used by the BloggingService. +* **Elasticsearch**: Used to write logs over Serilog. + +## Applications + +### Authentication Server (AuthServer.Host) + +This project is used by all other services and applications for authentication & single sign on. Mainly, uses **IdentityServer4** to provide these services. It uses some of the [pre-build ABP modules](../Modules/Index) like *Identity*, *Audit Logging* and *Permission Management*. + +#### Database & EF Core Configuration + +This application uses a SQL database (named it as **MsDemo_Identity**) and maintains its schema via **Entity Framework Core migrations.** + +It has a DbContext named **AuthServerDbContext** and defined as shown below: + +````csharp +public class AuthServerDbContext : AbpDbContext +{ + public AuthServerDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureIdentity(); + modelBuilder.ConfigureIdentityServer(); + modelBuilder.ConfigureAuditLogging(); + modelBuilder.ConfigurePermissionManagement(); + modelBuilder.ConfigureSettingManagement(); + } +} +```` + +In the **OnModelCreating**, you see **ConfigureX()** method calls. A module with a database schema generally declares such an extension method to configure EF Core mappings for its own entities. This is a flexible approach where you can arrange your databases and modules inside them; You can use a different database for each module, or combine some of them in a shared database. In the AuthServer project, we decided to combine multiple module schemas in a single EF Core DbContext, in a single physical database. These modules are Identity, IdentityServer, AuditLogging, PermissionManagement and SettingManagement modules. + +Notice that this DbContext is only for database migrations. All modules have their own `DbContext` classes those are used in the runtime by the modules. + +#### User Interface + +AuthServer has a simple home page that shows the current user info if the current user has logged in: + +![microservice-sample-authserver-home](../images/microservice-sample-authserver-home.png) + +It also provides Login & Register pages: + +![microservice-sample-authserver-login](../images/microservice-sample-authserver-login.png) + +These pages are not included in the project itself. Instead, AuthServer project uses the prebuilt ABP [account module](https://github.com/abpframework/abp/tree/master/modules/account) with IdentityServer extension. That means it can also act as an OpenId Connect server with necessary UI and logic. + +#### Dependencies + +* **RabbitMQ** for messaging to other services. +* **Redis** for distributed/shared caching. +* **Elasticsearch** for storing logs. + +### Backend Admin Application (BackendAdminApp.Host) + +This is a web application that is used to manage users, roles, permissions and products in the system. + +#### Authentication + +BackendAdminApp redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the backend application again. Authentication configuration is setup in the `BackendAdminAppHostModule` class: + +````charp +context.Services.AddAuthentication(options => +{ + options.DefaultScheme = "Cookies"; + options.DefaultChallengeScheme = "oidc"; +}) +.AddCookie("Cookies", options => +{ + options.Cookie.Expiration = TimeSpan.FromDays(365); + options.ExpireTimeSpan = TimeSpan.FromDays(365); +}) +.AddOpenIdConnect("oidc", options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ClientId = configuration["AuthServer:ClientId"]; + options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.RequireHttpsMetadata = false; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Add("role"); + options.Scope.Add("email"); + options.Scope.Add("phone"); + options.Scope.Add("BackendAdminAppGateway"); + options.Scope.Add("IdentityService"); + options.Scope.Add("ProductService"); + options.ClaimActions.MapAbpClaimTypes(); +}); +```` + +* It adds "Cookies" authentication as the primary authentication type. +* "oidc" authentication is configured to use the AuthServer application as the authentication server. +* It requires the additional identity scopes *role*, *email* and *phone*. +* It requires the API resource scopes *BackendAdminAppGateway*, *IdentityService* and *ProductService* because it will use these services as APIs. + +IdentityServer client settings are stored inside the `appsettings.json` file: + +````json +"AuthServer": { + "Authority": "http://localhost:64999", + "ClientId": "backend-admin-app-client", + "ClientSecret": "1q2w3e*" +} +```` + +#### User Interface + +The BackendAdminApp.Host project itself has not a single UI element/page. It is only used to serve UI pages of the Identity and Product Management modules. `BackendAdminAppHostModule` adds dependencies to `AbpIdentityWebModule` (*[Volo.Abp.Identity.Web](https://www.nuget.org/packages/Volo.Abp.Identity.Web)* package) and `ProductManagementWebModule` (*ProductManagement.Web* project) for that purpose. + +A screenshot from the user management page: + +![microservice-sample-backend-ui](../images/microservice-sample-backend-ui.png) + +A screenshot from the permission management modal for a role: + +![microservice-sample-backend-ui-permissions](../images/microservice-sample-backend-ui-permissions.png) + +#### Using Microservices + +Backend admin application uses the Identity and Product microservices for all operations, over the Backend Admin Gateway (BackendAdminAppGateway.Host). + +##### Remote End Point + +`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: + +````json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:65115/" + } +} +```` + +`http://localhost:65115/` is the URL of the *BackendAdminAppGateway.Host* project. It knows where are Identity and Product services are located. + +##### HTTP Clients + +ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. + +For that purpose, `BackendAdminAppHostModule` class declares dependencies for `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule`. + +Once you refer these client packages, you can directly inject an application service interface (e.g. `IIdentityUserAppService`) and use its methods like a local method call. It actually invokes remote service calls over HTTP to the related service endpoint. + +##### Passing the Access Token + +Since microservices requires authentication & authorization, each remote service call should contain an Authentication header. This header is obtained from the `access_token` inside the current `HttpContext` for the current user. This is automatically done when you use the `Volo.Abp.Http.Client.IdentityModel` package. `BackendAdminAppHostModule` declares dependencies to this package and to the related `AbpHttpClientIdentityModelModule` class. It is integrated to the HTTP Clients explained above. + +#### Dependencies + +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Public Web Site (PublicWebSite.Host) + +This is a public web site project that has a web blog and product list page. + +#### Authentication + +PublicWebSite can show blog posts and product list without login. If you login, you can also manage blogs. It redirects to the AuthServer for authentication. Once the user enters a correct username & password, the page is redirected to the public web site application again. Authentication configuration is setup in the `PublicWebSiteHostModule` class: + +```charp +context.Services.AddAuthentication(options => +{ + options.DefaultScheme = "Cookies"; + options.DefaultChallengeScheme = "oidc"; +}) +.AddCookie("Cookies", options => +{ + options.Cookie.Expiration = TimeSpan.FromDays(365); + options.ExpireTimeSpan = TimeSpan.FromDays(365); +}) +.AddOpenIdConnect("oidc", options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ClientId = configuration["AuthServer:ClientId"]; + options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.RequireHttpsMetadata = false; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Add("role"); + options.Scope.Add("email"); + options.Scope.Add("phone"); + options.Scope.Add("PublicWebSiteGateway"); + options.Scope.Add("ProductService"); + options.Scope.Add("BloggingService"); + options.ClaimActions.MapAbpClaimTypes(); +}); +``` + +- It adds "Cookies" authentication as the primary authentication type. +- "oidc" authentication is configured to use the AuthServer application as the authentication server. +- It requires the additional identity scopes *role*, *email* and *phone*. +- It requires the API resource scopes *PublicWebSiteGateway*, *BloggingService* and *ProductService* because it will use these services as APIs. + +IdentityServer client settings are stored inside the `appsettings.json` file: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ClientId": "public-website-client", + "ClientSecret": "1q2w3e*" +} +``` + +#### User Interface + +The PublicWebSite.Host project has a page to list products (`Pages/Products.cshtml`). It also uses the UI from the blogging module. `PublicWebSiteHostModule` adds dependencies to `BloggingWebModule` (*[Volo.Blogging.Web](https://www.nuget.org/packages/Volo.Blogging.Web)* package) for that purpose. + +A screenshot from the Products page: + +![microservice-sample-public-product-list](../images/microservice-sample-public-product-list.png) + +#### Using Microservices + +Publc web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host). + +##### Remote End Point + +`appsettings.json` file contains the `RemoteServices` section to declare the remote service endpoint(s). Each microservice will normally have different endpoints. However, this solution uses the API Gateway pattern to provide a single endpoint for the applications: + +```json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:64897/" + } +} +``` + +`http://localhost:64897/` is the URL of the *PublicWebSiteGateway.Host* project. It knows where are Blogging and Product services are located. + +##### HTTP Clients + +`PublicWebSiteHostModule` class declares dependencies for `BloggingHttpApiClientModule` and `ProductManagementHttpApiClientModule` to be able to use remote HTTP APIs for these services. + +##### Passing the Access Token + +Just like explained in the Backend Admin Application section, Public Web Site project also uses the `AbpHttpClientIdentityModelModule` to pass `access_token` to the calling services for authentication. + +#### Dependencies + +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Console Client Demo + +Finally, the solution includes a very simple console application, named ConsoleClientDemo, that uses Identity and Product services by authenticating through the AuthServer. It uses the Internal Gateway (InternalGateway.Host) to perform HTTP API calls. + +#### Remote Service Configuration + +`RemoteService` configuration in the `appsettings.json` file is simple: + +````json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:65129/" + } +} +```` + +`http://localhost:65129/` is the URL of the Internal Gateway. All API calls to the services are performed over this URL. + +#### Authentication (IdentityServer Client) Configuration + +`appsettings.json` also has a configuration for the IdentityServer authentication: + +````json +"IdentityClients": { + "Default": { + "GrantType": "client_credentials", + "ClientId": "console-client-demo", + "ClientSecret": "1q2w3e*", + "Authority": "http://localhost:64999", + "Scope": "InternalGateway IdentityService ProductService" + } +} +```` + +This sample uses the `client_credentials` grant type which requires a `ClientId` and `ClientSecret` for the authentication process. There are also [other grant types](http://docs.identityserver.io/en/latest/topics/grant_types.html). For example, you can use the following configuration to swith to the `password` (Resource Owner Password) grant type: + +````json +"IdentityClients": { + "Default": { + "GrantType": "password", + "ClientId": "console-client-demo", + "ClientSecret": "1q2w3e*", + "UserName": "admin", + "UserPassword": "1q2w3E*", + "Authority": "http://localhost:64999", + "Scope": "InternalGateway IdentityService ProductService" + } +} +```` + +Resource Owner Password requires a `UserName` & `UserPassword` in addition to client credentials. This grant type is useful to call remote services on behalf of a user. + +`Scope` declares the APIs (and the gateway) to grant access. This application uses the Internal Gateway. + +#### HTTP Client Dependencies + +`ConsoleClientDemoModule` has dependencies to `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule` in order to use Identity and Product APIs. It also has `AbpHttpClientIdentityModelModule` dependency to authenticate via IdentityServer. + +#### Using the Services + +Using the services is straightforward. See the `ClientDemoService` class which simply injects `IIdentityUserAppService` and `IProductAppService` and uses them. This class also shows a manual HTTP call using an `HttpClient` object. See source code of the `ClientDemoService` for details. + +## API Gateways / BFFs (Backend for Frontend) + +Gateways are used to provide a **single entry point** to the applications. In this way, an application only deal with a single service address (API endpoint) instead of a different addresses for each services. Gateways are also used for rate limiting, security, authentication, load balancing and many more requirements. + +"**Backend for Frontend**" (BFF) is a common architectural pattern which offers to build a **dedicated and specialized** gateway for each different application / client type. This solution uses this pattern and has multiple gateways. + +This solution uses the [Ocelot](https://github.com/ThreeMammals/Ocelot) library to build API Gateways. It's a widely accepted API Gateway library for ASP.NET Core. + +### Backend Admin Application Gateway (BackendAdminAppGateway.Host) + +This is backend (server side API) for the "Backend Admin Application" (don't confuse about the naming; Backend Admin Application is a frontend web application actually, but used by system admins rather than regular users). + +#### Authentication + +This gateway uses IdentityServer `Bearer` authentication and configured like that: + +````csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = + AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +```` + +`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). + +`ApiName` is the API which is being protected, `BackendAdminAppGateway` in this case. So, this solution defines gateways as API resources. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +````json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "BackendAdminAppGateway" +} +```` + +#### Ocelot Configuration + +Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: + +````json +"ReRoutes": [ + { + "DownstreamPathTemplate": "/api/identity/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 63568 + } + ], + "UpstreamPathTemplate": "/api/identity/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + }, + { + "DownstreamPathTemplate": "/api/productManagement/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 60244 + } + ], + "UpstreamPathTemplate": "/api/productManagement/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + } +], +"GlobalConfiguration": { + "BaseUrl": "http://localhost:65115" +} +```` + +`ReRoutes` is an array of URL mappings. `BaseUrl` in the `GlobalConfiguration` section is the URL of this gateway (Ocelot needs to know its own URL). See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the configuration. + +Ocelot is a finalizer ASP.NET Core middleware and should be written as the last item in the pipeline: + +````csharp +app.UseOcelot().Wait(); +```` + +It handles and redirects requests based on the configuration above. + +#### ABP Configuration Endpoints + +ABP provides some built-in APIs to get some configuration and information from the server. Examples: + +* `/api/abp/application-configuration` returns localization texts, permission and setting values (try http://localhost:65115/api/abp/application-configuration for this gateway). +* `/Abp/ServiceProxyScript` returns dynamic javascript proxies to call services from a javascript client (try http://localhost:65115/Abp/ServiceProxyScript for this gateway). + +These endpoints should be served by the gateway service, not by microservices. A microservice can only know permissions related to that microservice. But, once properly configured, gateway can aggregate permission values for multiple services as a single list which is more suitable for clients. + +For this purpose, the ASP.NET Core pipeline was configured to handle some specific routes via MVC, instead of Ocelot. To make this possible, MapWhen extension method is used like that: + +````csharp +app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") || + ctx.Request.Path.ToString().StartsWith("/Abp/"), + app2 => + { + app2.UseMvcWithDefaultRouteAndArea(); + }); + +app.UseOcelot().Wait(); +```` + +This configuration uses standard MVC middleware when request path starts with `/api/abp/` or `/Abp/`. + +#### Swagger + +This gateway is configured to use the [swagger UI](https://swagger.io/tools/swagger-ui/), a popular tool to discover & test HTTP APIs. Normally, Ocelot does not support to show APIs on the swagger, because it can not know details of each microservice API. But it is possible when you follow ABP layered module architecture [best practices](../Best-Practices/Index.md). + +`BackendAdminAppGatewayHostModule` adds dependency to `AbpIdentityHttpApiModule` (*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* package) and `ProductManagementHttpApiModule` (*ProductManagement.HttpApi* project) to include their HTTP API Controllers. In this way, swagger can discover them. While it references to the API layer, it does not reference to the implementation of application services, because they will be running in the related microservice endpoints and redirected by the Ocelot based on the request URL. + +Anyway, when you open the URL `http://localhost:65115/swagger/index.html`, you will see APIs of all configured microservices. + +#### Permission Management + +Backend Admin Application provides a permission management UI (seen before) and uses this gateway to get/set permissions. Permission management API is hosted inside the gateway, instead of a separate service. This is a design decision, but it could be hosted as another microservice if you would like. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Public Web Site Gateway (PublicWebSiteGateway.Host) + +This is backend (server side API gateway) for the "Public Web Site" application. + +#### Authentication + +This gateway uses IdentityServer `Bearer` authentication and configured like that: + +```csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = + AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +``` + +`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). + +`ApiName` is the API which is being protected, `PublicWebSiteGateway` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "PublicWebSiteGateway" +} +``` + +#### Ocelot Configuration + +Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: + +```json +"ReRoutes": [ + { + "DownstreamPathTemplate": "/api/productManagement/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 60244 + } + ], + "UpstreamPathTemplate": "/api/productManagement/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + }, + { + "DownstreamPathTemplate": "/api/blogging/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 62157 + } + ], + "UpstreamPathTemplate": "/api/blogging/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + } +], +"GlobalConfiguration": { + "BaseUrl": "http://localhost:64897" +} +``` + +See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the Ocelot configuration. + +#### Other + +See the "ABP Configuration Endpoints" and "Swagger" topics inside the "Backend Admin Application Gateway" section which are very similar for this gateway. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Internal Gateway (InternalGateway.Host) + +This gateway is not a BFF. It is designed for inter-microservice communication and is not exposed publicly. + +#### Authentication + +This gateway uses IdentityServer `Bearer` authentication and configured like that: + +```csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +``` + +`AddIdentityServerAuthentication` extension method comes from the [IdentityServer4.AccessTokenValidation](https://www.nuget.org/packages/IdentityServer4.AccessTokenValidation) package, part of the IdentityServer4 project (see [its documentation](http://docs.identityserver.io/en/latest/topics/apis.html)). + +`ApiName` is the API which is being protected, `InternalGateway` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "InternalGateway" +} +``` + +#### Ocelot Configuration + +Ocelot needs to know the real URLs of the microservices to be able to redirect HTTP requests. The configuration for this gateway is like below: + +```json +"ReRoutes": [ + { + "DownstreamPathTemplate": "/api/identity/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 63568 + } + ], + "UpstreamPathTemplate": "/api/identity/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + }, + { + "DownstreamPathTemplate": "/api/productManagement/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 60244 + } + ], + "UpstreamPathTemplate": "/api/productManagement/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + }, + { + "DownstreamPathTemplate": "/api/blogging/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 62157 + } + ], + "UpstreamPathTemplate": "/api/blogging/{everything}", + "UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post" ] + } +], +"GlobalConfiguration": { + "BaseUrl": "http://localhost:65129" +} +``` + +`ReRoutes` configuration covers all microservices in the system. See [its own documentation](https://ocelot.readthedocs.io/en/latest/features/configuration.html) to better understand the Ocelot configuration. + +#### Other + +See the "ABP Configuration Endpoints" and "Swagger" topics inside the "Backend Admin Application Gateway" section which are very similar for this gateway. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + ## Microservices -### Identity Service +Microservices are standalone HTTP APIs those implement the business of the system in a distributed manner. + +* They are used by applications and other microservices through the gateways and HTTP APIs. +* They can raise or register to events in the system. +* They can communicate to each other via asynchronous messaging. + +### Identity Service (IdentityService.Host) + +This service provides user and role management APIs. + +#### Database + +Shares the same database (MsDemo_Identity) with the AuthServer application. + +#### Identity Module + +This service actually just hosts the ABP Identity package/module. Does not include any API itself. In order to host it, adds the following dependencies: + +* `AbpIdentityHttpApiModule` (*[Volo.Abp.Identity.HttpApi](https://www.nuget.org/packages/Volo.Abp.Identity.HttpApi)* package) to provide Identity APIs. +* `AbpIdentityApplicationModule` (*[Volo.Abp.Identity.Application](https://www.nuget.org/packages/Volo.Abp.Identity.Application)* package) to host the implementation of the application and domain layers of the module. +* `AbpIdentityEntityFrameworkCoreModule` (*[Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* package) to use EF Core as database API. + +See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. + +#### Authentication + +This microservice uses IdentityServer `Bearer` authentication and configured like that: + +```csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = + AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +``` + +`ApiName` is the API which is being protected, `IdentityService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "IdentityService" +} +``` + +#### Swagger + +Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:63568/`, you are redirected to the swagger page to see and test the API. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Blogging Service (BloggingService.Host) + +This service hosts the blogging API. + +#### Database + +It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. It also uses the MsDemo_Identity SQL database for audit logs, permissions and settings. So, there are two connection strings in the `appsettings.json` file: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "Blogging": "mongodb://localhost|MsDemo_Blogging" +} +```` + +#### Blogging Module + +This service actually just hosts the ABP Blogging package/module. Does not include any API itself. In order to host it, adds the following dependencies: + +- `BloggingHttpApiModule` (*[Volo.Blogging.HttpApi](https://www.nuget.org/packages/Volo.Blogging.HttpApi)* package) to provide Blogging APIs. +- `BloggingApplicationModule` (*[Volo.Blogging.Application](https://www.nuget.org/packages/Volo.Blogging.Application)* package) to host the implementation of the application and domain layers of the module. +- `BloggingMongoDbModule` (*[Volo.Blogging.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore)* package) to use MongoDB as the database. + +See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. + +#### Authentication + +This microservice uses IdentityServer `Bearer` authentication and configured like that: + +```csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = + AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +``` + +`ApiName` is the API which is being protected, `BloggingService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "BloggingService" +} +``` + +#### IdentityServer Client + +This microservice also uses the Identity microservice API through the Internal Gateway, because it needs to query user details (username, email, phone, name and surname) in some cases. So, it is also a client for the IdentityServer and defines a section in the `appsettings.json` file for that: + +````json +"IdentityClients": { + "Default": { + "GrantType": "client_credentials", + "ClientId": "blogging-service-client", + "ClientSecret": "1q2w3e*", + "Authority": "http://localhost:64999", + "Scope": "InternalGateway IdentityService" + } +} +```` + +Since it uses the Internal Gateway, it should also configure the remote endpoint of the gateway: + +````json +"RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:65129/", + "UseCurrentAccessToken": "false" + } +} +```` + +When you set `UseCurrentAccessToken` to `false`, ABP ignores the current `access_token` in the current `HttpContext` and authenticates to the AuthServer with the credentials defined above. + +Why not using the token of the current user in the current request? Because, the user may not have required permissions on the Identity module, so it can not just pass the current authentication token directly to the Identity service. In addition, some of the blog service APIs are anonymous (not requires authenticated user), so in some cases there is no "current user" in the HTTP request. For these reasons, Blogging service should be defined as a client for the Identity service with its own credentials and permissions. + +If you check the `AbpPermissionGrants` table in the `MsDemo_Identity` database, you can see the related permission for the `blogging-service-client`. + +![microservice-sample-blogservice-permission-in-database](../images/microservice-sample-blogservice-permission-in-database.png) + +#### Swagger + +Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:62157/`, you are redirected to the swagger page to see and test the API. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +### Product Service (ProductService.Host) + +This service hosts the Product Management API. + +#### Database & EF Core Migrations + +It has a separated SQL database, named **MsDemo_ProductManagement**, for the product management module. It uses EF Core as the database provider and has a DbContext named `ProductServiceMigrationDbContext`: + +````csharp +public class ProductServiceMigrationDbContext : AbpDbContext +{ + public ProductServiceMigrationDbContext( + DbContextOptions options + ) : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureProductManagement(); + } +} +```` + +Actual model configuration is done inside the `modelBuilder.ConfigureProductManagement()` extension method. This project maintains the database schema using EF Core migrations. + +Notice that this DbContext is only for database migrations. Product Management module has its own `DbContext` class that is used in the runtime (See `ProductManagementDbContext` class in the ProductManagement.EntityFrameworkCore project). + +There are two connection strings in the `appsettings.json` file: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true" +} +```` + +`Default` connection strings points to the MsDemo_Identity database that is used for audit logging, permission and setting stores. `ProductManagement` connection string is used by the product module. + +#### Product Module + +This service actually just hosts the Product Management module. Does not include any API itself. In order to host it, adds the following dependencies: + +- `ProductManagementHttpApiModule` to provide product management APIs. +- `ProductManagementApplicationModule` to host the implementation of the application and domain layers of the module. +- `ProductManagementEntityFrameworkCoreModule` to use EF Core as database API. + +See the [module architecture best practice guide](../Best-Practices/Module-Architecture) to understand the layering better. See the Product Management module section below for more information about this module. + +#### Authentication + +This microservice uses IdentityServer `Bearer` authentication and configured like that: + +```csharp +context.Services.AddAuthentication("Bearer") +.AddIdentityServerAuthentication(options => +{ + options.Authority = configuration["AuthServer:Authority"]; + options.ApiName = configuration["AuthServer:ApiName"]; + options.RequireHttpsMetadata = false; + options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId; + options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role; + options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email; + options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified; + options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber; + options.InboundJwtClaimTypeMap["phone_number_verified"] = + AbpClaimTypes.PhoneNumberVerified; + options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName; +}); +``` + +`ApiName` is the API which is being protected, `ProductService` in this case. Rest of the configuration is related to claims mapping (which is planned to be automated in next ABP versions). The configuration related to authentication in the `appsettings.json` is simple: + +```json +"AuthServer": { + "Authority": "http://localhost:64999", + "ApiName": "ProductService" +} +``` + +#### Swagger + +Swagger UI is configured and is the default page for this service. If you navigate to the URL `http://localhost:60244/`, you are redirected to the swagger page to see and test the API. + +#### Dependencies + +- **RabbitMQ** for messaging to other services. +- **Redis** for distributed/shared caching. +- **Elasticsearch** for storing logs. + +## Modules + +ABP provides a strong infrastructure to make modular application development easier by providing services and architecture (see the [module development best practices guide](../Best-Practices/Index.md)). + +This solution demonstrate how to use [prebuilt application modules](../Modules/Index.md) in a distributed architecture. The solution also includes a simple "Product Management" module to show the implementation of a well layered module example. + +### Product Management + +Product Management is a module that consists of several layers and packages/projects: + +![microservice-sample-product-module-in-solution](../images/microservice-sample-product-module-in-solution.png) + +* `ProductManagement.Domain.Shared` contains constants and types shared among all layers. +* `ProductManagement.Domain` contains the domain logic and defines entities, domain services, domain events, business/domain exceptions. +* `ProductManagement.Application.Contracts` contains application service interfaces and DTOs. +* `ProductManagement.Application` contains the implementation of application services. +* `ProductManagement.EntityFrameworkCore` contains DbConext and other EF Core related classes and configuration. +* `ProductManagement.HttpApi` contains API Controllers. +* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. +* `ProductManagement.Web` contains the UI elements (pages, scripts, styles... etc). + +By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers. + +In this solution, Web layer runs in the Backend Admin Application while API layer is hosted by the Product microservice. + +This tutorial will highlight some important aspects of the module. But, it's suggested to see the source code for a better understanding. + +#### Domain Layer + +`Product` is the main [Aggregate Root](../Entities.md) of this module: + +````csharp +public class Product : AuditedAggregateRoot +{ + /// + /// A unique value for this product. + /// ProductManager ensures the uniqueness of it. + /// It can not be changed after creation of the product. + /// + [NotNull] + public string Code { get; private set; } + + [NotNull] + public string Name { get; private set; } + + public float Price { get; private set; } + + public int StockCount { get; private set; } + + //... +} +```` + +All of its properties have private setters which prevents any direct change of the properties from out of the class. Product class ensures its own integrity and validity by its own constructors and methods. + +It has two constructors: + +````csharp +private Product() +{ + //Default constructor is needed for ORMs. +} + +internal Product( + Guid id, + [NotNull] string code, + [NotNull] string name, + float price = 0.0f, + int stockCount = 0) +{ + Check.NotNullOrWhiteSpace(code, nameof(code)); + + if (code.Length >= ProductConsts.MaxCodeLength) + { + throw new ArgumentException( + $"Product code can not be longer than {ProductConsts.MaxCodeLength}" + ); + } + + Id = id; + Code = code; + SetName(Check.NotNullOrWhiteSpace(name, nameof(name))); + SetPrice(price); + SetStockCountInternal(stockCount, triggerEvent: false); +} + +```` + +Default (**parameterless**) constructor is private and is not used in the application code. It is needed because most ORMs requires a parameterless constructor on deserializing entities while getting from the database. + +Second constructor is **internal** that means it can only be used inside the domain layer. This enforces to use the `ProductManager` while creating a new `Product`. Because, `ProductManager` should implement a business rule on a new product creation. This constructor only requires the minimal required arguments to create a new product with some optional arguments. It checks some simple business rules to ensure that the entity is created as a valid product. + +Rest of the class has methods to manipulate properties of the entity. Example: + +````csharp +public Product SetPrice(float price) +{ + if (price < 0.0f) + { + throw new ArgumentException($"{nameof(price)} can not be less than 0.0!"); + } + + Price = price; + return this; +} + +```` + +`SetPrice` method is used to change the price of the product in a safe manner (by checking a validation rule). + +`SetStockCount` is another method that is used to change stock count of a product: + +````csharp +public Product SetStockCount(int stockCount) +{ + return SetStockCountInternal(stockCount); +} + +private Product SetStockCountInternal(int stockCount, bool triggerEvent = true) +{ + if (StockCount < 0) + { + throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!"); + } + + if (StockCount == stockCount) + { + return this; + } + + if (triggerEvent) + { + AddDistributedEvent( + new ProductStockCountChangedEto( + Id, StockCount, stockCount + ) + ); + } + + StockCount = stockCount; + return this; +} + +```` + +This method also triggers a **distributed event** with the `ProductStockCountChangedEto` parameter (Eto is a conventional postfix stands for **E**vent **T**ransfer **O**bject, but not required) to notify listeners that stock count of a product has changed. Any subscriber can receive this event and perform an action based on that knowledge. + +Events are distributed by RabbitMQ for this solution. But ABP is message broker independent by providing necessary abstractions (see the [Event Bus](../Event-Bus.md) document). + +As said before, this module forces to always use the `ProductManager` to create a new `Product`. `ProductManager` is a simple domain service defined as shown: + +````csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task CreateAsync( + [NotNull] string code, + [NotNull] string name, + float price = 0.0f, + int stockCount = 0) + { + var existingProduct = + await _productRepository.FirstOrDefaultAsync(p => p.Code == code); + + if (existingProduct != null) + { + throw new ProductCodeAlreadyExistsException(code); + } + + return await _productRepository.InsertAsync( + new Product( + GuidGenerator.Create(), + code, + name, + price, + stockCount + ) + ); + } +} +```` + +* It checks if given code is used before. Throws `ProductCodeAlreadyExistsException` so. +* If uses the `GuidGenerator` (`IGuidGenerator`) service to create a new `Guid`. +* It inserts the entity to the repository. + +So, with this design, uniqueness of the product code is guaranteed. + +`ProductCodeAlreadyExistsException` is a domain/business exception defined as like below: + +````csharp +public class ProductCodeAlreadyExistsException : BusinessException +{ + public ProductCodeAlreadyExistsException(string productCode) + : base("PM:000001", $"A product with code {productCode} has already exists!") + { + + } +} +```` + +`PM:000001` is a code for the exception type that is sent to the clients, so they can understand the error type. Not implemented for this case, but it is also possible to localize business exceptions. See the [exception handling documentation](../Exception-Handling.md). + +#### Application Layer + +Application layer of this module has two services: + +* `ProductAppService` is mainly used by the Backend Admin Application to manage (create, update, delete...) products. It requires permission to perform any operation. +* `PublicProductAppService` is used by the Public Web Site to show list of products to the visitors. It does not require any permission since most of the visitors are not logged in to the application. + +Notice that; instead of putting two application service into the same project, it might be a better principle to have separated application layers per application. But we unified them for simplicity in this solution. + +As an example, `ProductAppService` has the following method to update a product: + +````csharp +[Authorize(ProductManagementPermissions.Products.Update)] +public async Task UpdateAsync(Guid id, UpdateProductDto input) +{ + var product = await _productRepository.GetAsync(id); + + product.SetName(input.Name); + product.SetPrice(input.Price); + product.SetStockCount(input.StockCount); + + return ObjectMapper.Map(product); +} +```` + +* It defines the required permission (*ProductManagementPermissions.Products.Update* is a constant with value `ProductManagement.Update`) to perform this operation. +* Gets the id of the product and a DTO contains the values to update. +* Gets the related product entity from the repository. +* Uses the related methods (like `SetName`) of the `Product` class to change properties, because they are with private setters and the only way to change a value is to use an entity method. +* Returns an updated `ProductDto` to the client (client may need it for some reason) by using the [ObjectMapper](../Object-To-Object-Mapping.md). + +The implementation may vary based on the requirements. This implementation follows the [best practices offered here](../Best-Practices/Application-Services.md). + +#### Other Layers + +See other layers from the source code. + +## Infrastructure + +### Messaging and RabbitMQ + +Asynchronous Messaging is a key concept in distributed systems. It makes possible to communicate as a loosely coupled manner with fault tolerance. It does not require both sides to be online at the moment of messaging. So, it is a widely used communication pattern in microservice architecture. + +#### Distributed Event Bus + +Distributed Events (Event Bus) is a way of messaging where a service raise/trigger events while other services registers/listens to these events to be notified when an important event occurs. ABP makes distributed events easier to use by providing conventions, services and integrations. + +You have seen that the `Product` class publishing an event using the following code line: + +````csharp +AddDistributedEvent(new ProductStockCountChangedEto(Id, StockCount, stockCount)); +```` + +`ProductStockCountChangedEto` was defined as shown below: + +````csharp +[Serializable] +public class ProductStockCountChangedEto : EtoBase +{ + public Guid Id { get; } + + public int OldCount { get; set; } + + public int CurrentCount { get; set; } + + private ProductStockCountChangedEto() + { + //Default constructor is needed for deserialization. + } + + public ProductStockCountChangedEto(Guid id, int oldCount, int currentCount) + { + Id = id; + OldCount = oldCount; + CurrentCount = currentCount; + } +} +```` + +This object stores necessary information about the event. Another service can easily register to this event by implementing the `IDistributedEventHandler` interface with the generic `ProductStockCountChangedEto` parameter: + +````csharp +public class MyHandler : IDistributedEventHandler +{ + public async Task HandleEventAsync(ProductStockCountChangedEto eventData) + { + var productId = eventData.Id; + //... + } +} +```` + +All the integration and communication are done by the ABP framework when you use the [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) package. If you need to publish events out of an entity, just inject the `IDistributedEventBus` and use the `PublishAsync` method. + +See the [Event Bus](../Event-Bus.md) documentation for more information about the distributed event system. + +#### RabbitMQ Configuration + +In this solution, [RabbitMQ](https://www.rabbitmq.com/) is used for messaging & distributed events. + +[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) package is required to integrate to the RabbitMQ for distributed event system. Then you need to add dependency to the `AbpEventBusRabbitMqModule` for your module. For example, `ProductServiceHostModule` declares this dependency. + +`AbpEventBusRabbitMqModule` gets configuration from the `appsettings.json` by default. For example, the Product Service has such a configuration: + +````json +"RabbitMQ": { + "Connections": { + "Default": { + "HostName": "localhost" + } + }, + "EventBus": { + "ClientName": "MsDemo_ProductService", + "ExchangeName": "MsDemo" + } +} +```` + +### Caching and Redis + +A distributed system obviously needs to a distributed and shared cache, instead of isolated in-memory caches for each service. + +[Redis](https://redis.io/) is used as a distributed cache in this solution. The solution uses Microsoft's standard [Microsoft.Extensions.Caching.Redis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Redis) package for integration. All applications and services uses Redis cache when you use and configure this package. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more. + +The solution also uses the [Microsoft.AspNetCore.DataProtection.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.AspNetCore.DataProtection.StackExchangeRedis) package to share data protection keys between applications and services over Redis cache. + +### Logging, Serilog, Elasticsearch and Kibana + +This solution uses [Serilog](https://serilog.net/) as a logging library. It is a widely used library which has many data source integrations including [Elasticsearch](https://www.elastic.co/products/elasticsearch). + +Logging configurations are done in `Program.cs` files using a code block similar to the given below: + +````csharp +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.WithProperty("Application", "ProductService") + .Enrich.FromLogContext() + .WriteTo.File("Logs/logs.txt") + .WriteTo.Elasticsearch( + new ElasticsearchSinkOptions(new Uri(configuration["ElasticSearch:Url"])) + { + AutoRegisterTemplate = true, + AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6, + IndexFormat = "msdemo-log-{0:yyyy.MM}" + }) + .CreateLogger(); +```` + +This configures multiple log target: File and Elasticsearch. `Application` property is set to `ProductService` for this example. This is a way of distinguishing the logs of multiple services in a single database. You can then query logs by the `Application` name. + +Elasticsearch URL is read from the `appsettings.json` configuration file: + +````json +"ElasticSearch": { + "Url": "http://localhost:9200" +} +```` + +If you use Kibana, which is a Visualization tool that is well integrated to Elasticsearch, you can see some fancy UI about your logs: + +![microservice-sample-kibana-2](../images/microservice-sample-kibana-2.png) + +*Figure - A dashboard that shows log and error counts by service/application.* + +![microservice-sample-kibana-1](../images/microservice-sample-kibana-1.png) + +*Figure - A list of log entries* + +Kibana URL is `http://localhost:5601/` by default. + +### Audit Logging + +ABP provides automatic audit logging which saves every request in detail (who is the current user, what is the browser/client, what actions performed, which entities changed, even which properties of entities has been updated). See the [audit logging document](../Audit-Logging.md) for details. + +All of the services and applications are configured to write audit logs. Audit logs are saved to the MsDemo_Identity SQL database. So, you can query all audit logs of all applications from a single point. -... \ No newline at end of file +An Audit Log record has a `CorrelationId` property that can be used to track a request. When a service calls another service in a single web request, they both save audit logs with the same `CorrelationId`. See the `AbpAuditLogs` table in the database. \ No newline at end of file diff --git a/docs/en/images/microservice-sample-authserver-home.png b/docs/en/images/microservice-sample-authserver-home.png new file mode 100644 index 0000000000..7ae62ad6d5 Binary files /dev/null and b/docs/en/images/microservice-sample-authserver-home.png differ diff --git a/docs/en/images/microservice-sample-authserver-login.png b/docs/en/images/microservice-sample-authserver-login.png new file mode 100644 index 0000000000..f6e8dfaa9f Binary files /dev/null and b/docs/en/images/microservice-sample-authserver-login.png differ diff --git a/docs/en/images/microservice-sample-backend-ui-permissions.png b/docs/en/images/microservice-sample-backend-ui-permissions.png new file mode 100644 index 0000000000..02769be3e5 Binary files /dev/null and b/docs/en/images/microservice-sample-backend-ui-permissions.png differ diff --git a/docs/en/images/microservice-sample-backend-ui.png b/docs/en/images/microservice-sample-backend-ui.png new file mode 100644 index 0000000000..0935a6047a Binary files /dev/null and b/docs/en/images/microservice-sample-backend-ui.png differ diff --git a/docs/en/images/microservice-sample-blogservice-permission-in-database.png b/docs/en/images/microservice-sample-blogservice-permission-in-database.png new file mode 100644 index 0000000000..f99cd39893 Binary files /dev/null and b/docs/en/images/microservice-sample-blogservice-permission-in-database.png differ diff --git a/docs/en/images/microservice-sample-kibana-1.png b/docs/en/images/microservice-sample-kibana-1.png new file mode 100644 index 0000000000..f3a51bd349 Binary files /dev/null and b/docs/en/images/microservice-sample-kibana-1.png differ diff --git a/docs/en/images/microservice-sample-kibana-2.png b/docs/en/images/microservice-sample-kibana-2.png new file mode 100644 index 0000000000..31b281486c Binary files /dev/null and b/docs/en/images/microservice-sample-kibana-2.png differ diff --git a/docs/en/images/microservice-sample-product-module-in-solution.png b/docs/en/images/microservice-sample-product-module-in-solution.png new file mode 100644 index 0000000000..27e841cdd5 Binary files /dev/null and b/docs/en/images/microservice-sample-product-module-in-solution.png differ diff --git a/docs/en/images/microservice-sample-public-product-list.png b/docs/en/images/microservice-sample-public-product-list.png new file mode 100644 index 0000000000..cf8649bbe5 Binary files /dev/null and b/docs/en/images/microservice-sample-public-product-list.png differ diff --git a/docs/en/images/microservice-sample-solution.png b/docs/en/images/microservice-sample-solution.png new file mode 100644 index 0000000000..4c8344ed68 Binary files /dev/null and b/docs/en/images/microservice-sample-solution.png differ diff --git a/docs/zh-Hans/Domain-Services.md b/docs/zh-Hans/Domain-Services.md new file mode 100644 index 0000000000..181efe0c43 --- /dev/null +++ b/docs/zh-Hans/Domain-Services.md @@ -0,0 +1,3 @@ +# ABP Documentation + +待添加 diff --git a/docs/zh-Hans/Dynamic-Proxying-Interceptors.md b/docs/zh-Hans/Dynamic-Proxying-Interceptors.md new file mode 100644 index 0000000000..6657f1ca33 --- /dev/null +++ b/docs/zh-Hans/Dynamic-Proxying-Interceptors.md @@ -0,0 +1,3 @@ +## Dynamic Proxying / Interceptors + +待添加 diff --git a/docs/zh-Hans/Guid-Generation.md b/docs/zh-Hans/Guid-Generation.md new file mode 100644 index 0000000000..be0c1f425b --- /dev/null +++ b/docs/zh-Hans/Guid-Generation.md @@ -0,0 +1,3 @@ +## Guid 生成 + +待添加 diff --git a/docs/zh-Hans/Unit-Of-Work.md b/docs/zh-Hans/Unit-Of-Work.md new file mode 100644 index 0000000000..dc281a0974 --- /dev/null +++ b/docs/zh-Hans/Unit-Of-Work.md @@ -0,0 +1,3 @@ +## Unit of Work + +待添加 diff --git a/docs/zh-Hans/Validation.md b/docs/zh-Hans/Validation.md new file mode 100644 index 0000000000..c2b128f0bc --- /dev/null +++ b/docs/zh-Hans/Validation.md @@ -0,0 +1,3 @@ +## Validation + +待添加 diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index d9ac2aea60..56575a4755 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -67,7 +67,8 @@ "path": "Exception-Handling.md" }, { - "text": "验证" + "text": "验证", + "path": "Validation.md" }, { "text": "授权" @@ -160,7 +161,8 @@ "path": "Repositories.md" }, { - "text": "领域服务" + "text": "领域服务", + "path": "Domain-Services.md" }, { "text": "规约" @@ -178,7 +180,8 @@ "text": "数据传输对象(DTO)" }, { - "text": "工作单元" + "text": "工作单元", + "path": "Unit-Of-Work.md" } ] } @@ -189,7 +192,7 @@ "items": [ { "text": "API", - "items": [ + "items": [ { "text": "自动API控制器", "path": "AspNetCore/Auto-API-Controllers.md" diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperAttributeExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperAttributeExtensions.cs new file mode 100644 index 0000000000..ef581529a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Extensions/TagHelperAttributeExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions +{ + public static class TagHelperAttributeExtensions + { + public static string ToHtmlAttributeAsString(this TagHelperAttribute attribute) + { + return attribute.Name + "=\"" + attribute.Value + "\""; + } + + public static string ToHtmlAttributesAsString(this List attributes) + { + var attributesAsString = ""; + + foreach (var attribute in attributes) + { + attributesAsString += attribute.ToHtmlAttributeAsString() + " "; + } + + return attributesAsString; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index fa16acbd93..cdf1f30788 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -392,7 +392,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress) { var list = context.GetValue>(FormGroupContents) ?? new List(); - surpress = list != null; + surpress = list == null; if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\""))) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs index b63155a515..6ec2911714 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs @@ -160,7 +160,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress) { var list = context.GetValue>(FormGroupContents) ?? new List(); - surpress = list != null; + surpress = list == null; if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\""))) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs index 58a20f5224..5a095d561c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs @@ -308,7 +308,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual void AddGroupToFormGroupContents(TagHelperContext context, string propertyName, string html, int order, out bool surpress) { var list = context.GetValue>(FormGroupContents) ?? new List(); - surpress = list != null; + surpress = list == null; if (list != null && !list.Any(igc => igc.HtmlContent.Contains("id=\"" + propertyName.Replace('.', '_') + "\""))) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs index ead3367ac7..8c6a1a1d24 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelper.cs @@ -1,5 +1,9 @@ -namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid { + [HtmlTargetElement("abp-row")] + [HtmlTargetElement("abp-form-row")] public class AbpRowTagHelper : AbpTagHelper { public VerticalAlign VAlign { get; set; } = VerticalAlign.Default; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs index ecbe5db97e..0dc92f300d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Grid/AbpRowTagHelperService.cs @@ -7,8 +7,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid { public override void Process(TagHelperContext context, TagHelperOutput output) { + if (output.TagName == "abp-row") + { + output.Attributes.AddClass("row"); + } + if (output.TagName == "abp-form-row") + { + output.Attributes.AddClass("form-row"); + } + output.TagName = "div"; - output.Attributes.AddClass("row"); ProcessVerticalAlign(output); ProcessHorizontalAlign(output); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs index ee90724c1a..2de18d0592 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; @@ -13,7 +14,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var innerContent = await output.GetChildContentAsync(); var tabHeader = GetTabHeaderItem(context, output); - var tabContent = GetTabContentItem(innerContent.GetContent()); + var tabContent = GetTabContentItem(context, output, innerContent.GetContent()); var tabHeaderItems = context.GetValue>(TabItems); @@ -30,23 +31,31 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var link = TagHelper.Name; var control = TagHelper.Name; var title = TagHelper.Title; + var attributes = GetTabHeaderAttributes(context, output); + + var classAttributesAsString = attributes.Where(a=>a.Name == "class").ToList().Select(a=>a.Value).JoinAsString(" "); + var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString(); if (!string.IsNullOrWhiteSpace(TagHelper.ParentDropdownName)) { - return "" + title + ""; + return "" + title + ""; } - return "
  • " + + return "
  • " + title + "
  • "; } - protected virtual string GetTabContentItem(string content) + protected virtual string GetTabContentItem(TagHelperContext context, TagHelperOutput output, string content) { var headerId = TagHelper.Name + "-tab"; var id = TagHelper.Name; + var attributes = GetTabContentAttributes(context, output); + + var classAttributesAsString = attributes.Where(a => a.Name == "class").ToList().Select(a => a.Name).JoinAsString(" "); + var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString(); - return "
    " + + return "
    " + content + "
    "; } @@ -58,5 +67,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab TagHelper.Name = TabItemNamePlaceHolder; } } + + protected virtual List GetTabContentAttributes(TagHelperContext context, TagHelperOutput output) { + var contentprefix = "content-"; + return GetTabAttributesByPrefix(output.Attributes, contentprefix); + } + + protected virtual List GetTabHeaderAttributes(TagHelperContext context, TagHelperOutput output) { + var headerprefix = "header-"; + return GetTabAttributesByPrefix(output.Attributes, headerprefix); + } + + private List GetTabAttributesByPrefix(TagHelperAttributeList attributes, string prefix) { + return attributes.Where(a=>a.Name.StartsWith(prefix)) + .Select(a=> new TagHelperAttribute(a.Name.Substring(prefix.Length), a.Value)).ToList(); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml index 33419549f0..515d4cc7dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/Default.cshtml +++ b/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 - \ No newline at end of file +@if (!Model.CurrentUser.IsAuthenticated) +{ + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs index 739dc71b5b..bfcaebed0e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Components/TenantSwitch/TenantSwitchViewComponent.cs +++ b/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 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; } } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs index 07f24165e3..08d04cfed2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/AbpAspNetCoreMvcUIBasicThemeModule.cs +++ b/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)); }); }); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Bundling/BasicThemeGlobalScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Bundling/BasicThemeGlobalScriptContributor.cs new file mode 100644 index 0000000000..76ce8b2c05 --- /dev/null +++ b/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"); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml index 2e7e7f3f4c..7bfe8f1730 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml +++ b/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; - } - - + @menuItem.DisplayName + + + } } else { - } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml new file mode 100644 index 0000000000..fb8feb2ad8 --- /dev/null +++ b/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) + { + + @Model.DisplayName + + } +} +else +{ + +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css index ffcc7a2dae..3b5cc467fc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.css +++ b/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; + } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/wwwroot/themes/basic/layout.js new file mode 100644 index 0000000000..8a5b94c7c6 --- /dev/null +++ b/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; + }); +}); \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs index e5b169486f..bd54aef4d9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs +++ b/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; + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs index 4757ce2f93..c7c961176c 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Threading/HttpContextCancellationTokenProvider.cs +++ b/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; diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs index 78383d1252..e62f1ed2a4 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Tracing/AspNetCoreCorrelationIdProvider.cs +++ b/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() diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs index 00680e0398..84530e2aa6 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs +++ b/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); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs index 59be1c2ad5..2fdb60b75f 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs +++ b/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); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs index a9dbe670bb..d02bc76da5 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/CacheNameAttribute.cs +++ b/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() + .FirstOrDefault(); + + if (cacheNameAttribute != null) + { + return cacheNameAttribute.Name; + } + + return cacheItemType.FullName.RemovePostFix("CacheItem"); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index 708633b715..477b0c764b 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/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() - .FirstOrDefault(); - - CacheName = cacheNameAttribute != null ? cacheNameAttribute.Name : typeof(TCacheItem).FullName; + CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem)); //IgnoreMultiTenancy IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true); diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs new file mode 100644 index 0000000000..a16373eedc --- /dev/null +++ b/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(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 does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually."); + } + + public static TSource SetProperty(this TSource source, string name, object value) + where TSource : IHasExtraProperties + { + source.ExtraProperties[name] = value; + return source; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs index 69a4d9ddaa..b6558f34d1 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs @@ -75,11 +75,10 @@ namespace Volo.Abp.Domain.Entities where TEntity : IEntity { var lambdaParam = Expression.Parameter(typeof(TEntity)); - var lambdaBody = Expression.Equal( - Expression.PropertyOrField(lambdaParam, nameof(Entity.Id)), - Expression.Constant(id, typeof(TKey)) - ); - + var leftExpression = Expression.PropertyOrField(lambdaParam, "Id"); + Expression> closure = () => id; + var rightExpression = Expression.Convert(closure.Body, leftExpression.Type); + var lambdaBody = Expression.Equal(leftExpression, rightExpression); return Expression.Lambda>(lambdaBody, lambdaParam); } } diff --git a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs index 1d02ecb1e9..ab6320546c 100644 --- a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/NullCancellationTokenProvider.cs +++ b/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() { diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs index 7375376a06..e786c24f29 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Program.cs +++ b/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() .Build(); } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs index dd134465ee..52c11d7896 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs +++ b/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(); } } diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs index 7c04a9cf1b..fa069348cd 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/Authorization_Tests.cs +++ b/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); } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs index fdfd77b986..bb28b4ea99 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/TestServices/AuthorizationTestPermissionDefinitionProvider.cs +++ b/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"); } } } diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs index 32a954d430..0f790a03a3 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs +++ b/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; }); diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs index 9531711932..711e1e0bce 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs +++ b/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 { [Fact] - public async Task Configure_CacheOptions() + public void Configure_CacheOptions() { var personCache = GetRequiredService>(); - - 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] diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/ExtraProperties_Tests.cs new file mode 100644 index 0000000000..eea827fbee --- /dev/null +++ b/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 + { + + } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ExtraProperties_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ExtraProperties_Tests.cs new file mode 100644 index 0000000000..b389ad61b9 --- /dev/null +++ b/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 + { + + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index f1bb42a344..25886f77a6 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/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")); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ExtraProperties_Tests.cs new file mode 100644 index 0000000000..67eff13572 --- /dev/null +++ b/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 : TestAppTestBase + where TStartupModule : IAbpModule + { + protected readonly ICityRepository CityRepository; + + protected ExtraProperties_Tests() + { + CityRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_An_Extra_Property() + { + var london = await CityRepository.FindByNameAsync("London"); + london.HasProperty("Population").ShouldBeTrue(); + london.GetProperty("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("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("Population").ShouldBe(11_000_042); + } + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs index 132d7a81db..6a68ebc91a 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs +++ b/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(options => + { + options.FileSets.AddEmbedded("Volo.Abp.Account.Web"); + }); + } } } diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml new file mode 100644 index 0000000000..fa3efda621 --- /dev/null +++ b/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 + + +
    +
    +

    + @if (Model.ClientInfo.ClientLogoUrl != null) + { + + } + + @Model.ClientInfo.ClientName + is requesting your permission +

    +
    +
    +
    + +
    + + + +
    Uncheck the permissions you do not wish to grant.
    + + @if (Model.ConsentInput.IdentityScopes.Any()) + { +

    Personal Information

    + +
      + @for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++) + { +
    • +
      + +
      + @* TODO: Use attributes on the view model instead of using hidden here *@ + @if (Model.ConsentInput.IdentityScopes[i].Description != null) + { + + } +
    • + } +
    + } + + @if (Model.ConsentInput.ApiScopes.Any()) + { +

    Application Access

    + +
      + @for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++) + { +
    • +
      + +
      + @* TODO: Use attributes on the view model instead of using hidden here *@ + @if (Model.ConsentInput.ApiScopes[i].Description != null) + { + + } +
    • + } +
    + } + + @if (Model.ClientInfo.AllowRememberConsent) + { +
    + +
    + } + +
    + + + @if (Model.ClientInfo.ClientUrl != null) + { + + @Model.ClientInfo.ClientName + + } +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs new file mode 100644 index 0000000000..3fb68feaca --- /dev/null +++ b/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 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 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 IdentityScopes { get; set; } + + public List ApiScopes { get; set; } + + [Required] + public string UserDecision { get; set; } + + public bool RememberConsent { get; set; } + + public List 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; + } + } + } +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj index 6c81672b40..724ecc38ef 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj @@ -20,8 +20,10 @@ + + + - diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml index 8c54ce8005..9c438d8141 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/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 +

    @L["Login"]

    @if (Model.EnableLocalLogin) {
    diff --git a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs index 6a7ca9ec6d..1945c6ac31 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs +++ b/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(options => + { + options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files"); + options.FileUploadUrlRoot = "/files/"; + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/blogging/app/Volo.BloggingTestApp/Startup.cs b/modules/blogging/app/Volo.BloggingTestApp/Startup.cs index afeb27a51f..e4ccc31b60 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/Startup.cs +++ b/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(); } diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs index f77e25f570..e73dc334da 100644 --- a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/BloggingPermissions.cs +++ b/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 diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs index fffc9cdd5b..fbac86d827 100644 --- a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Blogs/IBlogAppService.cs +++ b/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> GetListPagedAsync(PagedAndSortedResultRequestDto input); - Task> GetListAsync(); Task GetByShortNameAsync(string shortName); diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/BloggingWebConsts.cs new file mode 100644 index 0000000000..ae4eb32dff --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadInputDto.cs new file mode 100644 index 0000000000..c0686835fd --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/FileUploadOutputDto.cs new file mode 100644 index 0000000000..1c3b1a1a94 --- /dev/null +++ b/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; } + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs b/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Files/IFileAppService.cs new file mode 100644 index 0000000000..6fed8bc62f --- /dev/null +++ b/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 UploadAsync(FileUploadInputDto input); + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj index a769735b31..95682a2a9e 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj +++ b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj @@ -10,6 +10,7 @@ + diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs index 790d47dff2..f06864a2d6 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Blogs/BlogAppService.cs @@ -18,17 +18,6 @@ namespace Volo.Blogging.Blogs _blogRepository = blogRepository; } - public async Task> GetListPagedAsync(PagedAndSortedResultRequestDto input) - { - var blogs = await _blogRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount ); - - var totalCount = await _blogRepository.GetTotalCount(); - - var dtos = ObjectMapper.Map, List>(blogs); - - return new PagedResultDto(totalCount, dtos); - } - public async Task> GetListAsync() { var blogs = await _blogRepository.GetListAsync(); diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs index 32b31da23a..0ff7bc46cc 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Comments/CommentAppService.cs +++ b/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>(comments)); } - //[Authorize(BloggingPermissions.Comments.Create)] TODO: Temporary removed + [Authorize] public async Task 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); } + [Authorize] public async Task UpdateAsync(Guid id, UpdateCommentDto input) { var comment = await _commentRepository.GetAsync(id); @@ -104,6 +104,7 @@ namespace Volo.Blogging.Comments return ObjectMapper.Map(comment); } + [Authorize] public async Task DeleteAsync(Guid id) { var comment = await _commentRepository.GetAsync(id); diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs index 855b504ae4..01e3fefd36 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/CommonOperations.cs +++ b/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 { diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/BlogFileOptions.cs new file mode 100644 index 0000000000..a06bdafc8d --- /dev/null +++ b/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; } + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileAppService.cs new file mode 100644 index 0000000000..8f18608080 --- /dev/null +++ b/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 options) + { + Options = options.Value; + } + + public virtual Task 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 + { + 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; + } + } +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/FileUploadConsts.cs new file mode 100644 index 0000000000..28cb3491f3 --- /dev/null +++ b/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 AllowedImageUploadFormats = new Collection + { + ImageFormat.Jpeg, + ImageFormat.Png, + ImageFormat.Gif, + ImageFormat.Bmp + }; + + public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString())); + } +} diff --git a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Helpers/ImageFormatHelper.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs similarity index 100% rename from modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Helpers/ImageFormatHelper.cs rename to modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Files/ImageFormatHelper.cs diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs index 69e325f0c7..8799b4e0d0 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Posts/PostAppService.cs +++ b/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(postDtos); } - private async Task> FilterPostsByTag(List allPostDtos, Tag tag) - { - var filteredPostDtos = new List(); - 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 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(tags.Split(",").Select(t => t.Trim())); } + + private Task> FilterPostsByTag(List allPostDtos, Tag tag) + { + var filteredPostDtos = new List(); + + foreach (var postDto in allPostDtos) + { + if (postDto.Tags.All(p => p.Id != tag.Id)) + { + continue; + } + + filteredPostDtos.Add(postDto); + } + + return Task.FromResult(filteredPostDtos); + } } } diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs index 473a8698df..0e6e75e071 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/Tagging/TagAppService.cs +++ b/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( ObjectMapper.Map, List>(postTags)); } diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs index 62ef6be7b3..27e01a0262 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Blogs/IBlogRepository.cs +++ b/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 FindByShortNameAsync(string shortName); - Task> GetListAsync(string sorting, int maxResultCount, int skipCount); - Task GetTotalCount(); } } diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs index 0ae80c8d42..9bdbb2fc0d 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUser.cs +++ b/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, IUser + public class BlogUser : AggregateRoot, 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; diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs index 7ef29a026d..9bf0025570 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users/BlogUserLookupService.cs +++ b/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) diff --git a/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs b/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs index e0e70578b1..5c9b43e8ed 100644 --- a/modules/blogging/src/Volo.Blogging.EntityFrameworkCore/Volo/Blogging/Blogs/EfCoreBlogRepository.cs +++ b/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> GetListAsync(string sorting, int maxResultCount, int skipCount) - { - var auditLogs = await DbSet.OrderBy(sorting ?? "creationTime desc") - .PageBy(skipCount, maxResultCount) - .ToListAsync(); - - return auditLogs; - } - public async Task GetTotalCount() { return await DbSet.CountAsync(); diff --git a/modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs b/modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs index 9fbb70f849..084afb958a 100644 --- a/modules/blogging/src/Volo.Blogging.HttpApi/Volo/Blogging/BlogsController.cs +++ b/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> GetListPagedAsync(PagedAndSortedResultRequestDto input) - { - return await _blogAppService.GetListPagedAsync(input); - } - - [HttpGet] - [Route("all")] public async Task> GetListAsync() { return await _blogAppService.GetListAsync(); diff --git a/modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj b/modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj index 4969dd630f..0d299c1f3d 100644 --- a/modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj +++ b/modules/blogging/src/Volo.Blogging.MongoDB/Volo.Blogging.MongoDB.csproj @@ -1,7 +1,9 @@  + + - netcoreapp2.2 + netstandard2.0 Volo.Blogging.MongoDB Volo.Blogging.MongoDB diff --git a/modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs b/modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs index 9319eb64af..8a3b759730 100644 --- a/modules/blogging/src/Volo.Blogging.MongoDB/Volo/Blogging/Blogs/MongoBlogRepository.cs +++ b/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> GetListAsync(string sorting, int maxResultCount, int skipCount) - { - var auditLogs = GetMongoQueryable().OrderBy(sorting ?? "creationTime desc").As>() - .PageBy(skipCount, maxResultCount) - .ToList(); - - return auditLogs; - } - public async Task GetTotalCount() { return await GetMongoQueryable().CountAsync(); diff --git a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs b/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs index 1f8b932a83..7d3e12f3cd 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Areas/Blog/Controllers/FilesController.cs +++ b/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 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)); } } } \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs deleted file mode 100644 index 491ea9d67b..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/BloggingWebConsts.cs +++ /dev/null @@ -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 AllowedImageUploadFormats = new Collection - { - 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); - } - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs deleted file mode 100644 index accb424215..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/FileService.cs +++ /dev/null @@ -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 SaveFormFileAndGetUrlAsync(IFormFile file) - { - var uniqueFileName = await SaveFileInternalAsync(file.FileName, file.AsBytes()); - return GetFileUrl(uniqueFileName); - } - - public async Task 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 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; - } - - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs index 669958d086..aab0832bbf 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/FormFileExtensions.cs +++ b/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)!"); - } + } } } \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs b/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs deleted file mode 100644 index 66516135bd..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/Hosting/IFileService.cs +++ /dev/null @@ -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 SaveFormFileAndGetUrlAsync(IFormFile file); - - Task SaveFileAsync(byte[] fileBytes, string originalFileName); - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs b/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs index 90395d763a..03214d1ee3 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/BloggingPage.cs +++ b/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 = "

    "; var closingTag = "

    "; diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs b/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs index 83edb281b0..20f281f8ae 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/Detail.cshtml.cs +++ b/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; diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js b/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js index 2d646de304..32bd7873ca 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blog/Posts/edit.js +++ b/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(); diff --git a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj index 6000771e97..8feb2057d7 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj @@ -19,7 +19,6 @@ -
    diff --git a/modules/docs/app/Volo.DocsTestApp/Program.cs b/modules/docs/app/Volo.DocsTestApp/Program.cs index 57ff6be5ea..470b5e6a98 100644 --- a/modules/docs/app/Volo.DocsTestApp/Program.cs +++ b/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() + .UseSerilog() .Build(); } } diff --git a/modules/docs/app/Volo.DocsTestApp/Startup.cs b/modules/docs/app/Volo.DocsTestApp/Startup.cs index e989f904cf..e6708d26e8 100644 --- a/modules/docs/app/Volo.DocsTestApp/Startup.cs +++ b/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(); } } diff --git a/modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj b/modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj index d1065535b9..b24f84e537 100644 --- a/modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj +++ b/modules/docs/app/Volo.DocsTestApp/Volo.DocsTestApp.csproj @@ -11,7 +11,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs index 1105fe94aa..9bf81c2140 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs @@ -14,7 +14,7 @@ namespace Volo.Docs.Admin CreateMap().Ignore(x => x.ExtraProperties); CreateMap () - .Ignore(x => x.GitHubAccessToken).Ignore(x => x.GitHubRootUrl); + .Ignore(x => x.GitHubAccessToken).Ignore(x => x.GitHubRootUrl).Ignore(x => x.GitHubUserAgent); } } } diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs index 3b8f8836f2..7df02eec0c 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs +++ b/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 { {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; } } } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs index ddcda61058..77c5321ad0 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs +++ b/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 { {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; } } } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs index b25ef2d76e..4aaedd579b 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs +++ b/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) } ); diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs index 20970ade0d..02ffac9ee9 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs @@ -68,7 +68,7 @@ namespace Volo.Docs.Projects protected virtual async Task> GetVersionsAsync(Project project) { var store = _documentStoreFactory.Create(project.DocumentStoreType); - var versions = await store.GetVersions(project); + var versions = await store.GetVersionsAsync(project); if (!versions.Any()) { diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs index 8ab0b3f332..1b001303b5 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/IDocumentStore.cs @@ -7,9 +7,9 @@ namespace Volo.Docs.Documents { public interface IDocumentStore : IDomainService { - Task GetDocument(Project project, string documentName, string version); + Task GetDocumentAsync(Project project, string documentName, string version); - Task> GetVersions(Project project); + Task> GetVersionsAsync(Project project); Task GetResource(Project project, string resourceName, string version); } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/FileSystem/Documents/FileSystemDocumentStore.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/FileSystem/Documents/FileSystemDocumentStore.cs index a8366554dc..87a74d1702 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/FileSystem/Documents/FileSystemDocumentStore.cs +++ b/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 GetDocument(Project project, string documentName, string version) + public async Task 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> GetVersions(Project project) + public Task> GetVersionsAsync(Project project) { return Task.FromResult(new List()); } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs index 5f92d565c2..5baa073b9c 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentStore.cs @@ -22,13 +22,15 @@ namespace Volo.Docs.GitHub.Documents { public const string Type = "GitHub"; - public virtual async Task GetDocument(Project project, string documentName, string version) + public virtual async Task GetDocumentAsync(Project project, string documentName, string version) { var token = project.GetGitHubAccessTokenOrNull(); var rootUrl = project.GetGitHubUrl(version); var rawRootUrl = CalculateRawRootUrl(rootUrl); var rawDocumentUrl = rawRootUrl + documentName; var commitHistoryUrl = project.GetGitHubUrlForCommitHistory() + documentName; + var userAgent = project.GetGithubUserAgentOrNull(); + var isNavigationDocument = documentName == project.NavigationDocumentName; var editLink = rootUrl.ReplaceFirst("/tree/", "/blob/") + documentName; var localDirectory = ""; var fileName = documentName; @@ -48,13 +50,14 @@ namespace Volo.Docs.GitHub.Documents Format = project.Format, LocalDirectory = localDirectory, FileName = fileName, - Contributors = await GetContributors(commitHistoryUrl, token), + Contributors = new List(), + //Contributors = !isNavigationDocument ? await GetContributors(commitHistoryUrl, token, userAgent): new List(), Version = version, - Content = await DownloadWebContentAsStringAsync(rawDocumentUrl, token) + Content = await DownloadWebContentAsStringAsync(rawDocumentUrl, token, userAgent) }; } - public async Task> GetVersions(Project project) + public async Task> GetVersionsAsync(Project project) { List versions; try @@ -87,7 +90,8 @@ namespace Volo.Docs.GitHub.Documents var rawRootUrl = CalculateRawRootUrl(project.GetGitHubUrl(version)); var content = await DownloadWebContentAsByteArrayAsync( rawRootUrl + resourceName, - project.GetGitHubAccessTokenOrNull() + project.GetGitHubAccessTokenOrNull(), + project.GetGithubUserAgentOrNull() ); return new DocumentResource(content); @@ -118,7 +122,7 @@ namespace Volo.Docs.GitHub.Documents { try { - var urlStartingAfterFirstSlash = url.Substring(url.IndexOf("github.com/",StringComparison.OrdinalIgnoreCase) + "github.com/".Length); + var urlStartingAfterFirstSlash = url.Substring(url.IndexOf("github.com/", StringComparison.OrdinalIgnoreCase) + "github.com/".Length); return urlStartingAfterFirstSlash.Substring(0, urlStartingAfterFirstSlash.IndexOf('/')); } catch (Exception) @@ -141,17 +145,22 @@ namespace Volo.Docs.GitHub.Documents } } - private async Task DownloadWebContentAsStringAsync(string rawUrl, string token) + private async Task DownloadWebContentAsStringAsync(string rawUrl, string token, string userAgent) { 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", "request"); + + webClient.Headers.Add("User-Agent", userAgent ?? ""); + + //TODO: SET TIMEOUT? return await webClient.DownloadStringTaskAsync(new Uri(rawUrl)); } @@ -164,16 +173,19 @@ namespace Volo.Docs.GitHub.Documents } } - private async Task DownloadWebContentAsByteArrayAsync(string rawUrl, string token) + private async Task DownloadWebContentAsByteArrayAsync(string rawUrl, string token, string userAgent) { try { - using (var webClient = new WebClient()) + Logger.LogInformation("Downloading content from Github (DownloadWebContentAsByteArrayAsync): " + rawUrl); + + using (var webClient = new GithubWebClient()) { if (!token.IsNullOrWhiteSpace()) { webClient.Headers.Add("Authorization", "token " + token); } + webClient.Headers.Add("User-Agent", userAgent ?? ""); return await webClient.DownloadDataTaskAsync(new Uri(rawUrl)); } @@ -186,13 +198,13 @@ namespace Volo.Docs.GitHub.Documents } } - private async Task> GetContributors(string url, string token) + private async Task> GetContributors(string url, string token, string userAgent) { var contributors = new List(); try { - var commitsJsonAsString = await DownloadWebContentAsStringAsync(url, token); + var commitsJsonAsString = await DownloadWebContentAsStringAsync(url, token, userAgent); var commits = JArray.Parse(commitsJsonAsString); @@ -200,25 +212,22 @@ namespace Volo.Docs.GitHub.Documents { var author = commit["author"]; - if (contributors.All(c => c.Username != (string) author["login"])) + contributors.Add(new DocumentContributor { - contributors.Add(new DocumentContributor - { - Username = (string)author["login"], - UserProfileUrl = (string)author["html_url"], - AvatarUrl = (string)author["avatar_url"] - }); - } + Username = (string)author["login"], + UserProfileUrl = (string)author["html_url"], + AvatarUrl = (string)author["avatar_url"] + }); } - contributors.Reverse(); + contributors = contributors.GroupBy(c => c.Username).OrderByDescending(c=>c.Count()) + .Select( c => c.FirstOrDefault()).ToList(); } catch (Exception ex) { Logger.LogWarning(ex.Message); } - - + return contributors; } @@ -228,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; + } + } } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Projects/ProjectGithubExtensions.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Projects/ProjectGithubExtensions.cs index 2e7f058391..3d2170a121 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Projects/ProjectGithubExtensions.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Projects/ProjectGithubExtensions.cs @@ -41,6 +41,12 @@ namespace Volo.Docs.GitHub.Projects return project.ExtraProperties["GitHubAccessToken"] as string; } + public static string GetGithubUserAgentOrNull([NotNull] this Project project) + { + CheckGitHubProject(project); + return project.ExtraProperties["GitHubUserAgent"] as string; + } + public static void SetGitHubAccessToken([NotNull] this Project project, string value) { CheckGitHubProject(project); diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index a070bddecc..55c1752994 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -45,7 +45,7 @@
    -
    - - - - -
    + + @if (Model.ProjectSelectItems.Count > 1) + { +
    +
    +
    +
    + +
    + + +
    +
    +
    + } @if (Model.VersionSelectItems.Any()) { @@ -88,12 +97,19 @@
    - + } +
    + + + + +
    + @if (Model.Navigation == null || Model.Navigation.Content.IsNullOrEmpty()) {
    diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index bd2c5e8485..a80aeb474c 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -31,6 +31,8 @@ namespace Volo.Docs.Pages.Documents.Project public List VersionSelectItems { get; private set; } + public List 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) { diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/AbpIdentityDomainSharedModule.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/AbpIdentityDomainSharedModule.cs index bc45febd8d..efb8a7130a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/AbpIdentityDomainSharedModule.cs +++ b/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; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs index ac989f8f4c..0b4c5b3fac 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs +++ b/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) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs index 1345ab89bd..0ff080cfad 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDataSeeder.cs +++ b/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; diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityDataSeeder_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityDataSeeder_Tests.cs index 515b335fc8..a8e31dd6e5 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityDataSeeder_Tests.cs +++ b/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(); } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj index 8964af3fcb..6894c3f60b 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo.Abp.IdentityServer.Domain.Shared.csproj @@ -14,7 +14,6 @@ - diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/AbpIdentityServerDomainSharedModule.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/AbpIdentityServerDomainSharedModule.cs index 77ad2c698a..141bd1a5fb 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/AbpIdentityServerDomainSharedModule.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/AbpIdentityServerDomainSharedModule.cs @@ -1,13 +1,14 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.IdentityServer.Localization; +using Volo.Abp.IdentityServer.Localization; using Volo.Abp.Localization; using Volo.Abp.Modularity; namespace Volo.Abp.IdentityServer { + [DependsOn( + typeof(AbpLocalizationModule) + )] public class AbpIdentityServerDomainSharedModule : AbpModule { - public override void ConfigureServices(ServiceConfigurationContext context) { Configure(options => diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientConsts.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientConsts.cs index d83138fd2f..312a6d70ef 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientConsts.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientConsts.cs @@ -8,15 +8,15 @@ public const int ClientNameMaxLength = 200; - public const int ClientUriMaxLength = 2000; + public const int ClientUriMaxLength = 300; - public const int LogoUriMaxLength = 2000; + public const int LogoUriMaxLength = 300; public const int DescriptionMaxLength = 1000; - public const int FrontChannelLogoutUriMaxLength = 2000; + public const int FrontChannelLogoutUriMaxLength = 300; - public const int BackChannelLogoutUriMaxLength = 2000; + public const int BackChannelLogoutUriMaxLength = 300; public const int ClientClaimsPrefixMaxLength = 200; diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientPropertyConsts.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientPropertyConsts.cs index 6535304596..44ab04e517 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientPropertyConsts.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientPropertyConsts.cs @@ -2,7 +2,7 @@ { public class ClientPropertyConsts { - public const int KeyMaxLength = 250; - public const int ValueMaxLength = 2000; + public const int KeyMaxLength = 64; + public const int ValueMaxLength = 128; } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientRedirectUriConsts.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientRedirectUriConsts.cs index d67bbea509..b04588fb27 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientRedirectUriConsts.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientRedirectUriConsts.cs @@ -2,6 +2,6 @@ { public class ClientRedirectUriConsts { - public const int RedirectUriMaxLength = 2000; + public const int RedirectUriMaxLength = 200; } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpCorsPolicyService.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpCorsPolicyService.cs new file mode 100644 index 0000000000..9113051a4a --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpCorsPolicyService.cs @@ -0,0 +1,57 @@ +using IdentityServer4.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IdentityServer.Clients; + +namespace Volo.Abp.IdentityServer +{ + public class AbpCorsPolicyService : ICorsPolicyService + { + public ILogger Logger { get; set; } + protected IHybridServiceScopeFactory HybridServiceScopeFactory { get; } + protected IDistributedCache Cache { get; } + + public AbpCorsPolicyService( + IDistributedCache cache, + IHybridServiceScopeFactory hybridServiceScopeFactory) + { + Cache = cache; + HybridServiceScopeFactory = hybridServiceScopeFactory; + Logger = NullLogger.Instance; + } + + public async Task IsOriginAllowedAsync(string origin) + { + var cacheItem = await Cache.GetOrAddAsync(AllowedCorsOriginsCacheItem.AllOrigins, CreateCacheItemAsync); + + var isAllowed = cacheItem.AllowedOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase); + + if (!isAllowed) + { + Logger.LogWarning($"Origin is not allowed: {origin}"); + } + + return isAllowed; + } + + protected virtual async Task CreateCacheItemAsync() + { + // doing this here and not in the ctor because: https://github.com/aspnet/AspNetCore/issues/2377 + using (var scope = HybridServiceScopeFactory.CreateScope()) + { + var clientRepository = scope.ServiceProvider.GetRequiredService(); + + return new AllowedCorsOriginsCacheItem + { + AllowedOrigins = (await clientRepository.GetAllDistinctAllowedCorsOriginsAsync()).ToArray() + }; + } + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs index 68fd77e354..9b7ea50fc1 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs @@ -12,7 +12,6 @@ namespace Volo.Abp.IdentityServer { [DependsOn( typeof(AbpIdentityServerDomainSharedModule), - typeof(AbpDddDomainModule), typeof(AbpAutoMapperModule), typeof(AbpIdentityDomainModule), typeof(AbpSecurityModule) diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItem.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItem.cs new file mode 100644 index 0000000000..0f9babd1c2 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItem.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.IdentityServer +{ + public class AllowedCorsOriginsCacheItem + { + public const string AllOrigins = "AllOrigins"; + + public string[] AllowedOrigins { get; set; } + } +} \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItemInvalidator.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItemInvalidator.cs new file mode 100644 index 0000000000..92583dc4df --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedCorsOriginsCacheItemInvalidator.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.EventBus; +using Volo.Abp.IdentityServer.Clients; + +namespace Volo.Abp.IdentityServer +{ + public class AllowedCorsOriginsCacheItemInvalidator : ILocalEventHandler>, ITransientDependency + { + protected IDistributedCache Cache { get; } + + public AllowedCorsOriginsCacheItemInvalidator(IDistributedCache cache) + { + Cache = cache; + } + + public async Task HandleEventAsync(EntityChangedEventData eventData) + { + await Cache.RemoveAsync(AllowedCorsOriginsCacheItem.AllOrigins); + } + } +} \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/ApiResources/IApiResourceRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/ApiResources/IApiResourceRepository.cs index 182a2d06ec..e229f361d5 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/ApiResources/IApiResourceRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/ApiResources/IApiResourceRepository.cs @@ -32,7 +32,5 @@ namespace Volo.Abp.IdentityServer.ApiResources bool includeDetails = false, CancellationToken cancellationToken = default ); - - Task GetTotalCount(); } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/IClientRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/IClientRepository.cs index ede17dd148..6b0e2816db 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/IClientRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/IClientRepository.cs @@ -23,6 +23,6 @@ namespace Volo.Abp.IdentityServer.Clients CancellationToken cancellationToken = default ); - Task GetTotalCount(); + Task> GetAllDistinctAllowedCorsOriginsAsync(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityResources/IIdentityResourceRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityResources/IIdentityResourceRepository.cs index af53d12d38..0ae9754b91 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityResources/IIdentityResourceRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityResources/IIdentityResourceRepository.cs @@ -27,7 +27,5 @@ namespace Volo.Abp.IdentityServer.IdentityResources bool includeDetails = true, CancellationToken cancellationToken = default ); - - Task GetTotalCountAsync(); } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerBuilderExtensions.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerBuilderExtensions.cs index 04ef77e652..2168b2067e 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerBuilderExtensions.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerBuilderExtensions.cs @@ -13,7 +13,8 @@ namespace Volo.Abp.IdentityServer return builder .AddClientStore() - .AddResourceStore(); + .AddResourceStore() + .AddCorsPolicyService(); } } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/ApiResources/ApiResourceRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/ApiResources/ApiResourceRepository.cs index bad674f435..7b6acd495a 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/ApiResources/ApiResourceRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/ApiResources/ApiResourceRepository.cs @@ -61,11 +61,6 @@ namespace Volo.Abp.IdentityServer.ApiResources .ToListAsync(GetCancellationToken(cancellationToken)); } - public virtual async Task GetTotalCount() - { - return await DbSet.CountAsync(); - } - public override async Task DeleteAsync(Guid id, bool autoSave = false, CancellationToken cancellationToken = default) { var scopeClaims = DbContext.Set().Where(sc => sc.ApiResourceId == id); diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/Clients/ClientRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/Clients/ClientRepository.cs index 62043562e8..e62e330029 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/Clients/ClientRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/Clients/ClientRepository.cs @@ -37,9 +37,13 @@ namespace Volo.Abp.IdentityServer.Clients .ToListAsync(GetCancellationToken(cancellationToken)); } - public virtual async Task GetTotalCount() + public async Task> GetAllDistinctAllowedCorsOriginsAsync(CancellationToken cancellationToken = default) { - return await DbSet.CountAsync(); + return await DbSet + .AsNoTracking() + .SelectMany(x => x.AllowedCorsOrigins.Select(y => y.Origin)) + .Distinct() + .ToListAsync(GetCancellationToken(cancellationToken)); } public override IQueryable WithDetails() diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/IdentityResources/IdentityResourceRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/IdentityResources/IdentityResourceRepository.cs index a6ff62fc05..a5237a6cc8 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/IdentityResources/IdentityResourceRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/IdentityResources/IdentityResourceRepository.cs @@ -56,10 +56,5 @@ namespace Volo.Abp.IdentityServer.IdentityResources .Where(x => x.Name == name) .FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); } - - public virtual async Task GetTotalCountAsync() - { - return await DbSet.CountAsync(); - } } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.MongoDB/Volo/Abp/IdentityServer/MongoDB/MongoClientRepository.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.MongoDB/Volo/Abp/IdentityServer/MongoDB/MongoClientRepository.cs index a946a97ff7..7f95e4c981 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.MongoDB/Volo/Abp/IdentityServer/MongoDB/MongoClientRepository.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.MongoDB/Volo/Abp/IdentityServer/MongoDB/MongoClientRepository.cs @@ -14,16 +14,26 @@ namespace Volo.Abp.IdentityServer.MongoDB { public class MongoClientRepository : MongoDbRepository, IClientRepository { - public MongoClientRepository(IMongoDbContextProvider dbContextProvider) : base(dbContextProvider) + public MongoClientRepository( + IMongoDbContextProvider dbContextProvider + ) : base( + dbContextProvider) { } - public virtual async Task FindByCliendIdAsync(string clientId, bool includeDetails = true, CancellationToken cancellationToken = default) + public virtual async Task FindByCliendIdAsync( + string clientId, + bool includeDetails = true, + CancellationToken cancellationToken = default) { return await GetMongoQueryable().FirstOrDefaultAsync(x => x.ClientId == clientId, GetCancellationToken(cancellationToken)); } - public virtual async Task> GetListAsync(string sorting, int skipCount, int maxResultCount, bool includeDetails = false, + public virtual async Task> GetListAsync( + string sorting, + int skipCount, + int maxResultCount, + bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetMongoQueryable() @@ -33,6 +43,16 @@ namespace Volo.Abp.IdentityServer.MongoDB .ToListAsync(GetCancellationToken(cancellationToken)); } + public async Task> GetAllDistinctAllowedCorsOriginsAsync( + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .SelectMany(x => x.AllowedCorsOrigins) + .Select(y => y.Origin) + .Distinct() + .ToListAsync(GetCancellationToken(cancellationToken)); + } + public virtual async Task GetTotalCount() { return await GetCountAsync(); diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AbpIdentityServerDomainTestBase.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AbpIdentityServerDomainTestBase.cs new file mode 100644 index 0000000000..009c6ca468 --- /dev/null +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/AbpIdentityServerDomainTestBase.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.IdentityServer +{ + public class AbpIdentityServerDomainTestBase : AbpIntegratedTest + { + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + } +} diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/CorsPolicyService_Tests.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/CorsPolicyService_Tests.cs new file mode 100644 index 0000000000..0aa1e835ac --- /dev/null +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/CorsPolicyService_Tests.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using IdentityServer4.Services; +using Shouldly; +using Volo.Abp.IdentityServer.Clients; +using Xunit; + +namespace Volo.Abp.IdentityServer +{ + public class CorsPolicyService_Tests : AbpIdentityServerDomainTestBase + { + private readonly ICorsPolicyService _corsPolicyService; + private readonly IClientRepository _clientRepository; + + public CorsPolicyService_Tests() + { + _corsPolicyService = GetRequiredService(); + _clientRepository = GetRequiredService(); + } + + [Fact] + public async Task IsOriginAllowedAsync() + { + (await _corsPolicyService.IsOriginAllowedAsync("https://client1-origin.com")).ShouldBeTrue(); + (await _corsPolicyService.IsOriginAllowedAsync("https://unknown-origin.com")).ShouldBeFalse(); + } + + [Fact] + public async Task IsOriginAllowedAsync_Should_Invalidate_Cache_On_Update() + { + //It does not exists before + (await _corsPolicyService.IsOriginAllowedAsync("https://new-origin.com")).ShouldBeFalse(); + + var client1 = await _clientRepository.FindByCliendIdAsync("ClientId1"); + client1.AddCorsOrigin("https://new-origin.com"); + await _clientRepository.UpdateAsync(client1); + + //It does exists now + (await _corsPolicyService.IsOriginAllowedAsync("https://new-origin.com")).ShouldBeTrue(); + } + } +} diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests.csproj b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests.csproj index a18c1b4b23..faa5eb21aa 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests.csproj +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.2 Volo.Abp.IdentityServer.EntityFrameworkCore.Tests Volo.Abp.IdentityServer.EntityFrameworkCore.Tests true @@ -15,9 +15,7 @@ - - - + diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/AbpIdentityServerTestEntityFrameworkCoreModule.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/AbpIdentityServerTestEntityFrameworkCoreModule.cs index 95d3c78a2e..77e20531db 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/AbpIdentityServerTestEntityFrameworkCoreModule.cs +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/AbpIdentityServerTestEntityFrameworkCoreModule.cs @@ -10,9 +10,12 @@ using Volo.Abp.Uow; namespace Volo.Abp.IdentityServer { - [DependsOn(typeof(AbpAutofacModule))] - [DependsOn(typeof(AbpIdentityServerEntityFrameworkCoreModule))] - [DependsOn(typeof(AbpIdentityEntityFrameworkCoreModule))] + [DependsOn( + typeof(AbpAutofacModule), + typeof(AbpIdentityEntityFrameworkCoreModule), + typeof(AbpIdentityServerEntityFrameworkCoreModule), + typeof(AbpIdentityServerTestBaseModule) + )] public class AbpIdentityServerTestEntityFrameworkCoreModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/ClientRepository_Tests.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/ClientRepository_Tests.cs new file mode 100644 index 0000000000..ef667aec3e --- /dev/null +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/ClientRepository_Tests.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.IdentityServer +{ + public class ClientRepository_Tests : ClientRepository_Tests + { + + } +} diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs index 44c37b93b7..a313c2717f 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs @@ -81,10 +81,8 @@ namespace Volo.Abp.IdentityServer.Clients //Assert resources.ShouldNotBe(null); - resources.ApiResources.Count.ShouldBe(1); - resources.ApiResources.First().Name.ShouldBe("Test-ApiResource-Name-1"); - resources.IdentityResources.First().Name.ShouldBe("Test-Identity-Resource-Name-1"); - resources.IdentityResources.First().Required.ShouldBe(true); + resources.ApiResources.Count.ShouldBeGreaterThan(0); + resources.ApiResources.Any(r => r.Name == "Test-ApiResource-Name-1").ShouldBeTrue(); } } } diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/IdentityResourceRepository_Tests.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/IdentityResourceRepository_Tests.cs new file mode 100644 index 0000000000..f6370c10b7 --- /dev/null +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.EntityFrameworkCore.Tests/Volo/Abp/IdentityServer/IdentityResourceRepository_Tests.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.IdentityServer +{ + public class IdentityResourceRepository_Tests : IdentityResourceRepository_Tests + { + + } +} diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.MongoDB.Tests/Volo/Abp/IdentityServer/AbpIdentityServerMongoDbTestModule.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.MongoDB.Tests/Volo/Abp/IdentityServer/AbpIdentityServerMongoDbTestModule.cs index cbaf71585e..b9da4bb039 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.MongoDB.Tests/Volo/Abp/IdentityServer/AbpIdentityServerMongoDbTestModule.cs +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.MongoDB.Tests/Volo/Abp/IdentityServer/AbpIdentityServerMongoDbTestModule.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; -using Mongo2Go; +using Mongo2Go; using Volo.Abp.Data; using Volo.Abp.IdentityServer.MongoDB; using Volo.Abp.Modularity; diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/AbpIdentityServerTestDataBuilder.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/AbpIdentityServerTestDataBuilder.cs index 42f76d44a0..0c18e85350 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/AbpIdentityServerTestDataBuilder.cs +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/AbpIdentityServerTestDataBuilder.cs @@ -90,7 +90,7 @@ namespace Volo.Abp.IdentityServer FrontChannelLogoutUri = nameof(Client.FrontChannelLogoutUri) }; - client.AddCorsOrigin(nameof(ClientCorsOrigin.Origin)); + client.AddCorsOrigin("https://client1-origin.com"); client.AddClaim(nameof(ClientClaim.Value), nameof(ClientClaim.Type)); client.AddGrantType(nameof(ClientGrantType.GrantType)); client.AddIdentityProviderRestriction(nameof(ClientIdPRestriction.Provider)); diff --git a/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/ClientRepository_Tests.cs b/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/ClientRepository_Tests.cs index 8770e7969c..86f69610f5 100644 --- a/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/ClientRepository_Tests.cs +++ b/modules/identityserver/test/Volo.Abp.IdentityServer.TestBase/Volo/Abp/IdentityServer/ClientRepository_Tests.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Shouldly; @@ -15,7 +13,7 @@ namespace Volo.Abp.IdentityServer { protected IClientRepository clientRepository { get; } - public ClientRepository_Tests() + protected ClientRepository_Tests() { clientRepository = ServiceProvider.GetRequiredService(); } @@ -25,5 +23,12 @@ namespace Volo.Abp.IdentityServer { (await clientRepository.FindByCliendIdAsync("ClientId2")).ShouldNotBeNull(); } + + [Fact] + public async Task GetAllDistinctAllowedCorsOriginsAsync() + { + var origins = await clientRepository.GetAllDistinctAllowedCorsOriginsAsync(); + origins.Any().ShouldBeTrue(); + } } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs index de99509fd6..4bb3423bd4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs @@ -12,6 +12,8 @@ namespace Volo.Abp.PermissionManagement public bool IsGranted { get; set; } - public List Providers { get; set; } + public List AllowedProviders { get; set; } + + public List GrantedProviders { get; set; } } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs index a1ec3e99fc..560c5b87f1 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs @@ -62,7 +62,8 @@ namespace Volo.Abp.PermissionManagement Name = permission.Name, DisplayName = permission.DisplayName.Localize(_stringLocalizerFactory), ParentName = permission.Parent?.Name, - Providers = new List() + AllowedProviders = permission.Providers, + GrantedProviders = new List() }; var grantInfo = await _permissionManager.GetAsync(permission.Name, providerName, providerKey); @@ -71,7 +72,7 @@ namespace Volo.Abp.PermissionManagement foreach (var provider in grantInfo.Providers) { - grantInfoDto.Providers.Add(new ProviderInfoDto + grantInfoDto.GrantedProviders.Add(new ProviderInfoDto { ProviderName = provider.Name, ProviderKey = provider.Key, diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/en.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/en.json index 0886572861..bb6999c100 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/en.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/en.json @@ -1,6 +1,8 @@ { "culture": "en", "texts": { - "Permissions": "Permissions" + "Permissions": "Permissions", + "OnlyProviderPermissons": "Only this provider", + "All": "All" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/pt-BR.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/pt-BR.json index 7783fff8f9..358cf16281 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/pt-BR.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/pt-BR.json @@ -2,6 +2,8 @@ { "culture": "pt-BR", "texts": { - "Permissions": "Permissões" + "Permissions": "Permissões", + "OnlyProviderPermissons": "Apenas este provedor", + "All": "Todos" } -} \ No newline at end of file +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/tr.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/tr.json index 061b93fd86..8da6bccd95 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/tr.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/tr.json @@ -1,6 +1,8 @@ { "culture": "tr", "texts": { - "Permissions": "İzinler" + "Permissions": "İzinler", + "OnlyProviderPermissons": "Sadece bu sağlayıcı", + "All": "Hepsi" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml index a2bc020495..16133d88e2 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml @@ -12,42 +12,35 @@ - + + + - - - - - - -
    - @for (var i = 0; i < Model.Groups.Count; i++) + + @for (var i = 0; i < Model.Groups.Count; i++) + { + +

    @Model.Groups[i].DisplayName

    +

    Permissions for "@Model.EntityDisplayName".

    +
    + + @for (var j = 0; j < Model.Groups[i].Permissions.Count; j++) { -
    - @for (var j = 0; j < Model.Groups[i].Permissions.Count; j++) - { - - - } -
    + + } -
    -
    - + + } + diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs index 55217b797a..0702c1e766 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs @@ -104,11 +104,13 @@ namespace Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement public bool IsGranted { get; set; } - public List Providers { get; set; } + public List AllowedProviders { get; set; } + + public List GrantedProviders { get; set; } public bool IsDisabled(string currentProviderName) { - return IsGranted && Providers.All(p => p.ProviderName != currentProviderName); + return IsGranted && GrantedProviders.All(p => p.ProviderName != currentProviderName); } public string GetShownName(string currentProviderName) @@ -121,7 +123,7 @@ namespace Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement return string.Format( "{0} ({1})", DisplayName, - Providers + GrantedProviders .Where(p => p.ProviderName != currentProviderName) .Select(p => p.ProviderName) .JoinAsString(", ") diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js index fa4815e16e..f32b00720e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js @@ -4,12 +4,12 @@ abp.modals.PermissionManagement = function () { function checkParents($tab, $checkBox) { - var parentName = $checkBox.closest('.form-check').attr('data-parent-name'); + var parentName = $checkBox.closest('.custom-checkbox').attr('data-parent-name'); if (!parentName) { return; } - $tab.find('.form-check') + $tab.find('.custom-checkbox') .filter('[data-permission-name="' + parentName + '"]') .find('input[type="checkbox"]') .each(function() { @@ -20,12 +20,12 @@ } function uncheckChildren($tab, $checkBox) { - var permissionName = $checkBox.closest('.form-check').attr('data-permission-name'); + var permissionName = $checkBox.closest('.custom-checkbox').attr('data-permission-name'); if (!permissionName) { return; } - $tab.find('.form-check') + $tab.find('.custom-checkbox') .filter('[data-parent-name="' + permissionName + '"]') .find('input[type="checkbox"]') .each(function () { diff --git a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs index 7333434fc1..09979f00e1 100644 --- a/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs +++ b/modules/users/src/Volo.Abp.Users.Domain/Volo/Abp/Users/UserLookupService.cs @@ -12,6 +12,8 @@ namespace Volo.Abp.Users where TUser : class, IUser where TUserRepository : IUserRepository { + protected bool SkipExternalLookupIfLocalUserExists { get; set; } = true; + public IExternalUserLookupServiceProvider ExternalUserLookupServiceProvider { get; set; } public ILogger> Logger { get; set; } @@ -30,9 +32,16 @@ namespace Volo.Abp.Users public async Task FindByIdAsync(Guid id, CancellationToken cancellationToken = default) { + var localUser = await _userRepository.FindAsync(id, cancellationToken: cancellationToken); + if (ExternalUserLookupServiceProvider == null) { - return await _userRepository.FindAsync(id, cancellationToken: cancellationToken); + return localUser; + } + + if (SkipExternalLookupIfLocalUserExists && localUser != null) + { + return localUser; } IUserData externalUser; @@ -42,8 +51,11 @@ namespace Volo.Abp.Users externalUser = await ExternalUserLookupServiceProvider.FindByIdAsync(id, cancellationToken); if (externalUser == null) { - //TODO: Instead of deleting, should be make it inactive or something like that? - await WithNewUowAsync(() => _userRepository.DeleteAsync(id, cancellationToken: cancellationToken)); + if (localUser != null) + { + //TODO: Instead of deleting, should be make it inactive or something like that? + await WithNewUowAsync(() => _userRepository.DeleteAsync(localUser, cancellationToken: cancellationToken)); + } return null; } @@ -51,14 +63,12 @@ namespace Volo.Abp.Users catch (Exception ex) { Logger.LogException(ex); - return await _userRepository.FindAsync(id, cancellationToken: cancellationToken); + return localUser; } - var localUser = await _userRepository.FindAsync(id, cancellationToken: cancellationToken); if (localUser == null) { await WithNewUowAsync(() => _userRepository.InsertAsync(CreateUser(externalUser), cancellationToken: cancellationToken)); - return await _userRepository.FindAsync(id, cancellationToken: cancellationToken); } @@ -76,9 +86,16 @@ namespace Volo.Abp.Users public async Task FindByUserNameAsync(string userName, CancellationToken cancellationToken = default) { + var localUser = await _userRepository.FindByUserNameAsync(userName, cancellationToken); + if (ExternalUserLookupServiceProvider == null) { - return await _userRepository.FindByUserNameAsync(userName, cancellationToken); + return localUser; + } + + if (SkipExternalLookupIfLocalUserExists && localUser != null) + { + return localUser; } IUserData externalUser; @@ -88,11 +105,10 @@ namespace Volo.Abp.Users externalUser = await ExternalUserLookupServiceProvider.FindByUserNameAsync(userName, cancellationToken); if (externalUser == null) { - var localExistingUser = await _userRepository.FindByUserNameAsync(userName, cancellationToken); - if (localExistingUser != null) + if (localUser != null) { //TODO: Instead of deleting, should be make it passive or something like that? - await WithNewUowAsync(() => _userRepository.DeleteAsync(localExistingUser.Id, cancellationToken: cancellationToken)); + await WithNewUowAsync(() => _userRepository.DeleteAsync(localUser, cancellationToken: cancellationToken)); } return null; @@ -101,10 +117,9 @@ namespace Volo.Abp.Users catch (Exception ex) { Logger.LogException(ex); - return await _userRepository.FindByUserNameAsync(userName, cancellationToken); + return localUser; } - var localUser = await _userRepository.FindByUserNameAsync(userName, cancellationToken); if (localUser == null) { await WithNewUowAsync(() => _userRepository.InsertAsync(CreateUser(externalUser), cancellationToken: cancellationToken)); diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 6ac3b2c3d2..5a4aeb71e8 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -164,6 +164,7 @@ $projects = ( "modules/blogging/src/Volo.Blogging.Domain", "modules/blogging/src/Volo.Blogging.Domain.Shared", "modules/blogging/src/Volo.Blogging.EntityFrameworkCore", + "modules/blogging/src/Volo.Blogging.MongoDB", "modules/blogging/src/Volo.Blogging.HttpApi", "modules/blogging/src/Volo.Blogging.HttpApi.Client", "modules/blogging/src/Volo.Blogging.Web", diff --git a/samples/BookStore/src/Acme.BookStore.Web/Startup.cs b/samples/BookStore/src/Acme.BookStore.Web/Startup.cs index fffc45b3e6..0cd4c700d7 100644 --- a/samples/BookStore/src/Acme.BookStore.Web/Startup.cs +++ b/samples/BookStore/src/Acme.BookStore.Web/Startup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Serilog; using Volo.Abp; namespace Acme.BookStore @@ -21,15 +20,6 @@ namespace Acme.BookStore public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { - loggerFactory - .AddConsole() - .AddDebug() - .AddSerilog(new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.File("Logs/logs.txt") - .CreateLogger() - ); - app.InitializeApplication(); } } diff --git a/samples/MicroserviceDemo/README.md b/samples/MicroserviceDemo/README.md index e855adba0f..612d1d0153 100644 --- a/samples/MicroserviceDemo/README.md +++ b/samples/MicroserviceDemo/README.md @@ -1,26 +1,4 @@ -# MicroserviceDemo +# Microservice Demo Solution -Run demo using docker-compose: +This sample aims to demonstrate a simple yet complete microservice solution. See the [documentation](https://abp.io/documents/abp/latest/Samples/Microservice-Demo). -First restore mssql database -```sh -$ docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database -``` - -Build and start containers -```sh -$ docker-compose up -d -``` - -Add this line to your `hosts` file -``` -127.0.0.1 auth-server -``` -- Windows: `C:\Windows\System32\Drivers\etc\hosts` -- Linux & macOS: `/etc/hosts` - - -### Accessing the Web User Interfaces -- AuthServer: `http://localhost:64999` -- PublicWebsite: `http://localhost:53435` -- BackendAdminApp: `http://localhost:51954` \ No newline at end of file diff --git a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj index 4f10fd61b9..89752802a7 100644 --- a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj +++ b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj @@ -17,6 +17,7 @@ + @@ -40,4 +41,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs index bc0d0b825d..56d6c4900c 100644 --- a/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs +++ b/samples/MicroserviceDemo/applications/AuthServer.Host/AuthServerHostModule.cs @@ -1,6 +1,8 @@ using AuthServer.Host.EntityFrameworkCore; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; using Volo.Abp; using Volo.Abp.Account.Web; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; @@ -67,6 +69,11 @@ namespace AuthServer.Host options.IsEnabledForGetRequests = true; options.ApplicationName = "AuthServer"; }); + + //TODO: ConnectionMultiplexer.Connect call has problem since redis may not be ready when this service has started! + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj index 01bc6634d5..8da540107a 100644 --- a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj +++ b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj @@ -19,6 +19,7 @@ + @@ -42,4 +43,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs index 5969d69370..0bb625414a 100644 --- a/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs +++ b/samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminAppHostModule.cs @@ -1,9 +1,11 @@ using System; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using ProductManagement; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.AspNetCore.Authentication.OAuth; @@ -83,6 +85,10 @@ namespace BackendAdminApp.Host { options.Configuration = configuration["Redis:Configuration"]; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/applications/ConsoleClientDemo/Dockerfile b/samples/MicroserviceDemo/applications/ConsoleClientDemo/Dockerfile deleted file mode 100644 index 5822384ef6..0000000000 --- a/samples/MicroserviceDemo/applications/ConsoleClientDemo/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM microsoft/dotnet:2.2-aspnetcore-runtime-alpine AS base -WORKDIR /app -EXPOSE 80 - -FROM microsoft/dotnet:2.2-sdk-alpine AS build -WORKDIR /src -COPY . . -WORKDIR "/src/samples/MicroserviceDemo/applications/ConsoleClientDemo" -RUN dotnet restore -nowarn:msb3202,nu1503 -RUN dotnet build --no-restore -c Release -o /app - -FROM build AS publish -RUN dotnet publish --no-restore -c Release -o /app - -FROM base AS final -WORKDIR /app -COPY --from=publish /app . -ENTRYPOINT ["dotnet", "ConsoleClientDemo.dll"] diff --git a/samples/MicroserviceDemo/applications/ConsoleClientDemo/appsettings.json b/samples/MicroserviceDemo/applications/ConsoleClientDemo/appsettings.json index 3817ac17ec..336020aec8 100644 --- a/samples/MicroserviceDemo/applications/ConsoleClientDemo/appsettings.json +++ b/samples/MicroserviceDemo/applications/ConsoleClientDemo/appsettings.json @@ -6,11 +6,9 @@ }, "IdentityClients": { "Default": { - "GrantType": "password", + "GrantType": "client_credentials", "ClientId": "console-client-demo", "ClientSecret": "1q2w3e*", - "UserName": "admin", - "UserPassword": "1q2w3E*", "Authority": "http://localhost:64999", "Scope": "InternalGateway IdentityService ProductService" } diff --git a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj index 1585146a68..169b3d9e8d 100644 --- a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj +++ b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj @@ -18,6 +18,7 @@ + @@ -38,4 +39,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs index 3591fc018d..c4f565dd5d 100644 --- a/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs +++ b/samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSiteHostModule.cs @@ -1,9 +1,11 @@ using System; using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using ProductManagement; +using StackExchange.Redis; using Volo.Abp; using Volo.Abp.AspNetCore.Authentication.OAuth; using Volo.Abp.AspNetCore.Mvc.Client; @@ -75,6 +77,10 @@ namespace PublicWebSite.Host { options.Configuration = configuration["Redis:Configuration"]; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/databases/Dockerfile b/samples/MicroserviceDemo/databases/Dockerfile index 91879f9e7c..dad3238039 100644 --- a/samples/MicroserviceDemo/databases/Dockerfile +++ b/samples/MicroserviceDemo/databases/Dockerfile @@ -16,6 +16,7 @@ WORKDIR /src COPY --from=build /src/samples/MicroserviceDemo/microservices/ProductService.Host ./ProductService.Host COPY --from=build /src/samples/MicroserviceDemo/applications/AuthServer.Host ./AuthServer.Host COPY --from=build /src/samples/MicroserviceDemo/databases/entrypoint.sh . +RUN /bin/bash -c "sed -i $'s/\r$//' entrypoint.sh" RUN chmod +x ./entrypoint.sh ENTRYPOINT ["./entrypoint.sh"] diff --git a/samples/MicroserviceDemo/databases/MsDemo_Identity.zip b/samples/MicroserviceDemo/databases/MsDemo_Identity.zip index 18c8dc12ab..f31c9e4f3d 100644 Binary files a/samples/MicroserviceDemo/databases/MsDemo_Identity.zip and b/samples/MicroserviceDemo/databases/MsDemo_Identity.zip differ diff --git a/samples/MicroserviceDemo/databases/restore/Dockerfile b/samples/MicroserviceDemo/databases/restore/Dockerfile index 917fc27e1a..d0598eed28 100644 --- a/samples/MicroserviceDemo/databases/restore/Dockerfile +++ b/samples/MicroserviceDemo/databases/restore/Dockerfile @@ -10,5 +10,6 @@ WORKDIR /src COPY --from=unzip /src/MsDemo_Identity.bak . COPY --from=unzip /src/MsDemo_ProductManagement.bak . COPY /samples/MicroserviceDemo/databases/restore/entrypoint.sh . +RUN /bin/bash -c "sed -i $'s/\r$//' entrypoint.sh" RUN chmod +x ./entrypoint.sh ENTRYPOINT [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/samples/MicroserviceDemo/databases/restore/entrypoint.sh b/samples/MicroserviceDemo/databases/restore/entrypoint.sh index cb34a14ff7..91de230f78 100644 --- a/samples/MicroserviceDemo/databases/restore/entrypoint.sh +++ b/samples/MicroserviceDemo/databases/restore/entrypoint.sh @@ -10,20 +10,22 @@ until /opt/mssql-tools/bin/sqlcmd -S sqlserver -U SA -P $SA_PASSWORD -Q 'SELECT sleep 1 done -/opt/mssql-tools/bin/sqlcmd -S sqlserver \ - -U SA -P $SA_PASSWORD \ - -Q 'RESTORE FILELISTONLY FROM DISK = "/var/opt/mssql/backup/MsDemo_Identity.bak"' \ - | tr -s ' ' | cut -d ' ' -f 1-2 - -/opt/mssql-tools/bin/sqlcmd -S sqlserver \ - -U SA -P $SA_PASSWORD \ - -Q 'RESTORE FILELISTONLY FROM DISK = "/var/opt/mssql/backup/MsDemo_ProductManagement.bak"' \ - | tr -s ' ' | cut -d ' ' -f 1-2 +/opt/mssql-tools/bin/sqlcmd \ + -S sqlserver -U SA -P $SA_PASSWORD \ + -Q 'RESTORE DATABASE MsDemo_Identity FROM DISK = "/var/opt/mssql/backup/MsDemo_Identity.bak" WITH REPLACE, + MOVE "MsDemo_Identity" TO "/var/opt/mssql/data/MsDemo_Identity.mdf", + MOVE "MsDemo_Identity_log" TO "/var/opt/mssql/data/MsDemo_Identity_log.ldf"' /opt/mssql-tools/bin/sqlcmd \ -S sqlserver -U SA -P $SA_PASSWORD \ - -Q 'RESTORE DATABASE MsDemo_Identity FROM DISK = "/var/opt/mssql/backup/MsDemo_Identity.bak" WITH MOVE "MsDemo_Identity" TO "/var/opt/mssql/data/MsDemo_Identity.mdf", MOVE "MsDemo_Identity_log" TO "/var/opt/mssql/data/MsDemo_Identity_log.ldf"' + -Q 'RESTORE DATABASE MsDemo_ProductManagement FROM DISK = "/var/opt/mssql/backup/MsDemo_ProductManagement.bak" WITH REPLACE, + MOVE "MsDemo_ProductManagement" TO "/var/opt/mssql/data/MsDemo_ProductManagement.mdf", + MOVE "MsDemo_ProductManagement_log" TO "/var/opt/mssql/data/MsDemo_ProductManagement_log.ldf"' /opt/mssql-tools/bin/sqlcmd \ -S sqlserver -U SA -P $SA_PASSWORD \ - -Q 'RESTORE DATABASE MsDemo_ProductManagement FROM DISK = "/var/opt/mssql/backup/MsDemo_ProductManagement.bak" WITH MOVE "MsDemo_ProductManagement" TO "/var/opt/mssql/data/MsDemo_ProductManagement.mdf", MOVE "MsDemo_ProductManagement_log" TO "/var/opt/mssql/data/MsDemo_ProductManagement_log.ldf"' + -d MsDemo_Identity \ + -Q 'UPDATE IdentityServerClientRedirectUris SET RedirectUri = "http://localhost:51512/signin-oidc" WHERE ClientId = "00265494-2D70-9615-4BD0-39EB2AF3CD33" + UPDATE IdentityServerClientRedirectUris SET RedirectUri = "http://localhost:51513/signin-oidc" WHERE ClientId = "10265494-2D70-9615-4BD0-39EB2AF3CD33" + UPDATE IdentityServerClientPostLogoutRedirectUris SET PostLogoutRedirectUri = "http://localhost:51512/signout-callback-oidc" WHERE ClientId = "00265494-2D70-9615-4BD0-39EB2AF3CD33" + UPDATE IdentityServerClientPostLogoutRedirectUris SET PostLogoutRedirectUri = "http://localhost:51513/signout-callback-oidc" WHERE ClientId = "10265494-2D70-9615-4BD0-39EB2AF3CD33"' \ No newline at end of file diff --git a/samples/MicroserviceDemo/docker-compose.migrations.yml b/samples/MicroserviceDemo/docker-compose.migrations.yml index 513c1245aa..16d112e2b7 100644 --- a/samples/MicroserviceDemo/docker-compose.migrations.yml +++ b/samples/MicroserviceDemo/docker-compose.migrations.yml @@ -6,7 +6,7 @@ services: - SA_PASSWORD=yourStrong(!)Password - ACCEPT_EULA=Y ports: - - "1433:1433" + - "1433" migrations: image: 'microservice-demo/migrations:${TAG:-latest}' diff --git a/samples/MicroserviceDemo/docker-compose.override.yml b/samples/MicroserviceDemo/docker-compose.override.yml index 04efc436ec..c0fdaf0bf6 100644 --- a/samples/MicroserviceDemo/docker-compose.override.yml +++ b/samples/MicroserviceDemo/docker-compose.override.yml @@ -5,20 +5,20 @@ services: - SA_PASSWORD=yourStrong(!)Password - ACCEPT_EULA=Y ports: - - 1433:1433 + - "1433" mongodb: ports: - - 27017:27017 + - "27017" rabbitmq: ports: - - 15672:15672 - - 5672:5672 + - "15672" + - "5672" redis: ports: - - 6379:6379 + - "6379" elasticsearch: volumes: @@ -26,7 +26,7 @@ services: environment: - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ports: - - 9200:9200 + - "9200" logstash: volumes: @@ -35,32 +35,32 @@ services: environment: - "LS_JAVA_OPTS=-Xmx256m -Xms256m" ports: - - 9600:9600 - - 5000:5000 + - "9600" + - "5000" kibana: volumes: - ./elk/kibana/config/:/usr/share/kibana/config:ro ports: - - 5601:5601 + - 51510:5601 internal-gateway: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - - ReRoutes__0__DownstreamHostAndPorts__Host=identity-service - - ReRoutes__0__DownstreamHostAndPorts__Port=80 - - ReRoutes__1__DownstreamHostAndPorts__Host=product-service - - ReRoutes__1__DownstreamHostAndPorts__Port=80 - - ReRoutes__2__DownstreamHostAndPorts__Host=blogging-service - - ReRoutes__2__DownstreamHostAndPorts__Port=80 + - ReRoutes__0__DownstreamHostAndPorts__0__Host=identity-service + - ReRoutes__0__DownstreamHostAndPorts__0__Port=80 + - ReRoutes__1__DownstreamHostAndPorts__0__Host=product-service + - ReRoutes__1__DownstreamHostAndPorts__0__Port=80 + - ReRoutes__2__DownstreamHostAndPorts__0__Host=blogging-service + - ReRoutes__2__DownstreamHostAndPorts__0__Port=80 - GlobalConfiguration__BaseUrl=http://internal-gateway ports: - - 65129:80 + - "80" backend-admin-app-gateway: environment: @@ -68,110 +68,101 @@ services: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - RemoteServices__Default__BaseUrl=http://backend-admin-app-gateway/ - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - - ReRoutes__0__DownstreamHostAndPorts__Host=identity-service - - ReRoutes__0__DownstreamHostAndPorts__Port=80 - - ReRoutes__1__DownstreamHostAndPorts__Host=product-service - - ReRoutes__1__DownstreamHostAndPorts__Port=80 + - ReRoutes__0__DownstreamHostAndPorts__0__Host=identity-service + - ReRoutes__0__DownstreamHostAndPorts__0__Port=80 + - ReRoutes__1__DownstreamHostAndPorts__0__Host=product-service + - ReRoutes__1__DownstreamHostAndPorts__0__Port=80 - GlobalConfiguration__BaseUrl=http://backend-admin-app-gateway ports: - - 65115:80 + - 51531:80 public-website-gateway: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - - ReRoutes__0__DownstreamHostAndPorts__Host=product-service - - ReRoutes__0__DownstreamHostAndPorts__Port=80 - - ReRoutes__1__DownstreamHostAndPorts__Host=blogging-service - - ReRoutes__1__DownstreamHostAndPorts__Port=80 + - ReRoutes__0__DownstreamHostAndPorts__0__Host=product-service + - ReRoutes__0__DownstreamHostAndPorts__0__Port=80 + - ReRoutes__1__DownstreamHostAndPorts__0__Host=blogging-service + - ReRoutes__1__DownstreamHostAndPorts__0__Port=80 - GlobalConfiguration__BaseUrl=http://public-website-gateway ports: - - 64897:80 + - 51532:80 blogging-service: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - ConnectionStrings__Blogging=mongodb://mongodb|MsDemo_Blogging - Redis__Configuration=redis - RabbitMQ__Connections__Default__HostName=rabbitmq + - RemoteServices__Default__BaseUrl=http://internal-gateway/ ports: - - 62157:80 + - 51521:80 identity-service: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - RabbitMQ__Connections__Default__HostName=rabbitmq ports: - - 63568:80 + - 51522:80 product-service: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - - ConnectionStrings__ProductManagement=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false + - ConnectionStrings__ProductManagement=Server=sqlserver;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - RabbitMQ__Connections__Default__HostName=rabbitmq ports: - - 60244:80 + - 51523:80 auth-server: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:64999 + - ASPNETCORE_URLS=http://0.0.0.0:51511 - ConnectionStrings__Default=Server=sqlserver;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password;Integrated Security=false - Redis__Configuration=redis - RabbitMQ__Connections__Default__HostName=rabbitmq ports: - - 64999:64999 + - 51511:51511 backend-admin-app: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - RemoteServices__Default__BaseUrl=http://backend-admin-app-gateway/ - Redis__Configuration=redis ports: - - 51954:80 - - console-client-demo: - environment: - - ElasticSearch__Url=http://elasticsearch:9200 - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=http://0.0.0.0:80 - - RemoteServices__Default__BaseUrl=http://internal-gateway/ - - IdentityClients__Default__Authority=http://auth-server:64999 - ports: - - 63898:80 + - 51512:80 public-website: environment: - ElasticSearch__Url=http://elasticsearch:9200 - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - AuthServer__Authority=http://auth-server:64999 + - AuthServer__Authority=http://auth-server:51511 - RemoteServices__Default__BaseUrl=http://public-website-gateway/ - Redis__Configuration=redis ports: - - 53435:80 \ No newline at end of file + - 51513:80 \ No newline at end of file diff --git a/samples/MicroserviceDemo/docker-compose.yml b/samples/MicroserviceDemo/docker-compose.yml index 08c6874366..9a3e4e8191 100644 --- a/samples/MicroserviceDemo/docker-compose.yml +++ b/samples/MicroserviceDemo/docker-compose.yml @@ -117,15 +117,6 @@ services: - elasticsearch - backend-admin-app-gateway - console-client-demo: - image: 'microservice-demo/console-client-demo:${TAG:-latest}' - build: - context: ../../ - dockerfile: samples/MicroserviceDemo/applications/ConsoleClientDemo/Dockerfile - depends_on: - - elasticsearch - - internal-gateway - public-website: image: 'microservice-demo/public-website:${TAG:-latest}' build: diff --git a/samples/MicroserviceDemo/docs/design/Overall.md b/samples/MicroserviceDemo/docs/design/Overall.md deleted file mode 100644 index fe75084afe..0000000000 --- a/samples/MicroserviceDemo/docs/design/Overall.md +++ /dev/null @@ -1,21 +0,0 @@ -# Microservice Demo Overall - -## Introduction - -The goal of this work is to show how to create a complete microservice solution based on the ABP framework. - -## Tooling - -* **[ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-2.2)** as the web framework. -* **[ABP](https://abp.io)** as the application framework. -* **[Ocelot](https://github.com/ThreeMammals/Ocelot)** as the API Gateway. -* **[IdentityServer4](https://identityserver.io/)** as the authentication server/framework. -* [**Redis**](https://redis.io/) for distributed cache. -* [**RabbitMQ**](https://www.rabbitmq.com/) for distributed messaging. -* **[Serilog](https://serilog.net/)** for logging. -* **[Elasticsearch](https://www.elastic.co/products/elasticsearch)** as log database. -* **[Kibana](https://www.elastic.co/products/kibana)** as visualization tool (for logs in the Elasticsearch). - -## Diagram - -![diagram](diagram.png) \ No newline at end of file diff --git a/samples/MicroserviceDemo/docs/design/diagram.png b/samples/MicroserviceDemo/docs/design/diagram.png deleted file mode 100644 index b1d9f6c66e..0000000000 Binary files a/samples/MicroserviceDemo/docs/design/diagram.png and /dev/null differ diff --git a/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj b/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj index 1ec9405f2a..abaca0567b 100644 --- a/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj +++ b/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj @@ -20,6 +20,7 @@ + @@ -51,4 +52,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs b/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs index ddc484b2da..660419662c 100644 --- a/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs +++ b/samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Middleware; using ProductManagement; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Autofac; @@ -77,6 +79,10 @@ namespace BackendAdminAppGateway.Host { options.Configuration = configuration["Redis:Configuration"]; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj b/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj index 8b56b69df7..80b4b6ebb0 100644 --- a/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj +++ b/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj @@ -20,12 +20,14 @@ + + @@ -44,5 +46,13 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGatewayHostModule.cs b/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGatewayHostModule.cs index 68c98a26a2..943eaf0fc9 100644 --- a/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGatewayHostModule.cs +++ b/samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGatewayHostModule.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Middleware; using ProductManagement; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Autofac; @@ -13,12 +15,14 @@ using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.EntityFrameworkCore; using Volo.Abp.Security.Claims; using Volo.Abp.SettingManagement.EntityFrameworkCore; +using Volo.Blogging; namespace InternalGateway.Host { [DependsOn( typeof(AbpAutofacModule), typeof(AbpIdentityHttpApiModule), + typeof(BloggingHttpApiModule), typeof(ProductManagementHttpApiModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpPermissionManagementEntityFrameworkCoreModule), @@ -64,6 +68,10 @@ namespace InternalGateway.Host { options.Configuration = configuration["Redis:Configuration"]; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj b/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj index 6cddd67e53..cd41412f26 100644 --- a/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj +++ b/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj @@ -20,6 +20,7 @@ + @@ -44,4 +45,12 @@ + + + + signed + + + + \ No newline at end of file diff --git a/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGatewayHostModule.cs b/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGatewayHostModule.cs index 36b52cc1e3..88e74045d0 100644 --- a/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGatewayHostModule.cs +++ b/samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGatewayHostModule.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Middleware; using ProductManagement; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Autofac; @@ -64,6 +66,10 @@ namespace PublicWebSiteGateway.Host { options.Configuration = configuration["Redis:Configuration"]; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj index 10b797c0f4..5d3dc6cddb 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj @@ -19,6 +19,7 @@ + @@ -43,4 +44,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs index 1ae68cfd55..da076cf6e0 100644 --- a/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingServiceHostModule.cs @@ -1,7 +1,10 @@ using System; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Auditing; @@ -21,6 +24,7 @@ using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.Threading; using Volo.Blogging; using Volo.Blogging.Blogs; +using Volo.Blogging.Files; using Volo.Blogging.MongoDB; namespace BloggingService.Host @@ -43,6 +47,7 @@ namespace BloggingService.Host public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); context.Services.AddAuthentication("Bearer") .AddIdentityServerAuthentication(options => @@ -77,6 +82,12 @@ namespace BloggingService.Host options.UseSqlServer(); }); + Configure(options => + { + options.FileUploadLocalFolder = Path.Combine(hostingEnvironment.WebRootPath, "files"); + options.FileUploadUrlRoot = "/files/"; + }); + context.Services.AddDistributedRedisCache(options => { options.Configuration = configuration["Redis:Configuration"]; @@ -87,6 +98,10 @@ namespace BloggingService.Host options.IsEnabledForGetRequests = true; options.ApplicationName = "BloggingService"; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj index 600dbb96df..4be0500443 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj @@ -19,6 +19,7 @@ + @@ -40,4 +41,12 @@ + + + + signed + + + + diff --git a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs index 9dd68c9f39..2affd03367 100644 --- a/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityServiceHostModule.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Auditing; @@ -78,6 +80,10 @@ namespace IdentityService.Host options.IsEnabledForGetRequests = true; options.ApplicationName = "IdentityService"; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/EntityFrameworkCore/AuthServerDbContextFactory.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/EntityFrameworkCore/ProductServiceMigrationDbContextFactory.cs similarity index 100% rename from samples/MicroserviceDemo/microservices/ProductService.Host/EntityFrameworkCore/AuthServerDbContextFactory.cs rename to samples/MicroserviceDemo/microservices/ProductService.Host/EntityFrameworkCore/ProductServiceMigrationDbContextFactory.cs diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductService.Host.csproj b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductService.Host.csproj index cd3d255d3c..2b19692585 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductService.Host.csproj +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductService.Host.csproj @@ -19,6 +19,7 @@ + @@ -40,9 +41,12 @@ - - - - + + + + signed + + + diff --git a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs index c09fb4fcc1..6e08ad8e93 100644 --- a/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs +++ b/samples/MicroserviceDemo/microservices/ProductService.Host/ProductServiceHostModule.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using ProductManagement; using ProductManagement.EntityFrameworkCore; +using StackExchange.Redis; using Swashbuckle.AspNetCore.Swagger; using Volo.Abp; using Volo.Abp.Auditing; @@ -78,6 +80,10 @@ namespace ProductService.Host options.IsEnabledForGetRequests = true; options.ApplicationName = "ProductService"; }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + context.Services.AddDataProtection() + .PersistKeysToStackExchangeRedis(redis, "MsDemo-DataProtection-Keys"); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/ProductAppService.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/ProductAppService.cs index 90d16ec7b5..c993f27f02 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/ProductAppService.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/ProductAppService.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; namespace ProductManagement { @@ -11,9 +15,9 @@ namespace ProductManagement public class ProductAppService : ApplicationService, IProductAppService { private readonly ProductManager _productManager; - private readonly IProductRepository _productRepository; + private readonly IRepository _productRepository; - public ProductAppService(ProductManager productManager, IProductRepository productRepository) + public ProductAppService(ProductManager productManager, IRepository productRepository) { _productManager = productManager; _productRepository = productRepository; @@ -23,7 +27,11 @@ namespace ProductManagement { await NormalizeMaxResultCountAsync(input); - var products = await _productRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount); + var products = await _productRepository + .OrderBy(input.Sorting) + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + .ToListAsync(); var totalCount = await _productRepository.GetCountAsync(); diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/PublicProductAppService.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/PublicProductAppService.cs index 866cc31b34..b1f9f3fafc 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/PublicProductAppService.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application/ProductManagement/PublicProductAppService.cs @@ -1,15 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; namespace ProductManagement { public class PublicProductAppService : ApplicationService, IPublicProductAppService { - private readonly IProductRepository _productRepository; + private readonly IRepository _productRepository; - public PublicProductAppService(IProductRepository productRepository) + public PublicProductAppService(IRepository productRepository) { _productRepository = productRepository; } diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/IProductRepository.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/IProductRepository.cs deleted file mode 100644 index c6a1c865c1..0000000000 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/IProductRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Volo.Abp.Domain.Repositories; - -namespace ProductManagement -{ - public interface IProductRepository : IBasicRepository - { - Task> GetListAsync(string sorting, int maxResultCount, int skipCount); - } -} diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Product.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Product.cs index 15fff0cd69..bca0a7b09d 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Product.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Product.cs @@ -79,7 +79,7 @@ namespace ProductManagement private Product SetStockCountInternal(int stockCount, bool triggerEvent = true) { - if (StockCount < 0.0f) + if (StockCount < 0) { throw new ArgumentException($"{nameof(stockCount)} can not be less than 0!"); } @@ -91,7 +91,13 @@ namespace ProductManagement if (triggerEvent) { - AddDistributedEvent(new ProductStockCountChangedEto(StockCount, stockCount)); + AddDistributedEvent( + new ProductStockCountChangedEto( + Id, + StockCount, + stockCount + ) + ); } StockCount = stockCount; diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/ProductStockCountChangedEto.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/ProductStockCountChangedEto.cs index c456a7c135..272eade5f2 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/ProductStockCountChangedEto.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/ProductStockCountChangedEto.cs @@ -6,6 +6,8 @@ namespace ProductManagement [Serializable] public class ProductStockCountChangedEto : EtoBase { + public Guid Id { get; } + public int OldCount { get; set; } public int CurrentCount { get; set; } @@ -15,8 +17,9 @@ namespace ProductManagement //Default constructor is needed for deserialization. } - public ProductStockCountChangedEto(int oldCount, int currentCount) + public ProductStockCountChangedEto(Guid id, int oldCount, int currentCount) { + Id = id; OldCount = oldCount; CurrentCount = currentCount; } diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EfCoreProductRepository.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EfCoreProductRepository.cs deleted file mode 100644 index c229505c36..0000000000 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EfCoreProductRepository.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using ProductManagement.EntityFrameworkCore; -using Volo.Abp.Domain.Repositories.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; -using System.Linq; -using System.Linq.Dynamic.Core; -using Microsoft.EntityFrameworkCore; - -namespace ProductManagement -{ - public class EfCoreProductRepository : EfCoreRepository, IProductRepository - { - public EfCoreProductRepository(IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task> GetListAsync(string sorting, int maxResultCount, int skipCount) - { - // TODO: refactor sorting - var products = await DbSet.OrderBy(sorting ?? "creationTime desc") - .PageBy(skipCount, maxResultCount) - .ToListAsync(); - - return products; - } - } -} diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EntityFrameworkCore/ProductManagementDbContext.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EntityFrameworkCore/ProductManagementDbContext.cs index 03a3d52394..910f73a45e 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EntityFrameworkCore/ProductManagementDbContext.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.EntityFrameworkCore/ProductManagement/EntityFrameworkCore/ProductManagementDbContext.cs @@ -29,6 +29,5 @@ namespace ProductManagement.EntityFrameworkCore options.Schema = Schema; }); } - } } \ No newline at end of file diff --git a/samples/MicroserviceDemo/modules/product/src/ProductManagement.HttpApi/ProductManagement/ProductsController.cs b/samples/MicroserviceDemo/modules/product/src/ProductManagement.HttpApi/ProductManagement/ProductsController.cs index 96e129ccb4..c5fb4fcaa3 100644 --- a/samples/MicroserviceDemo/modules/product/src/ProductManagement.HttpApi/ProductManagement/ProductsController.cs +++ b/samples/MicroserviceDemo/modules/product/src/ProductManagement.HttpApi/ProductManagement/ProductsController.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; namespace ProductManagement { diff --git a/samples/MicroserviceDemo/modules/product/test/ProductManagement.Application.Tests/ProductManagement/ProductAppService_Tests.cs b/samples/MicroserviceDemo/modules/product/test/ProductManagement.Application.Tests/ProductManagement/ProductAppService_Tests.cs index 4964f5df98..2cdd7a0a61 100644 --- a/samples/MicroserviceDemo/modules/product/test/ProductManagement.Application.Tests/ProductManagement/ProductAppService_Tests.cs +++ b/samples/MicroserviceDemo/modules/product/test/ProductManagement.Application.Tests/ProductManagement/ProductAppService_Tests.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; using Xunit; namespace ProductManagement @@ -12,13 +11,13 @@ namespace ProductManagement public class ProductAppService_Tests : ProductManagementApplicationTestBase { private readonly IProductAppService _productAppService; - private readonly IProductRepository _productRepository; + private readonly IRepository _productRepository; private readonly ProductManagementTestData _testData; public ProductAppService_Tests() { _productAppService = GetRequiredService(); - _productRepository = GetRequiredService(); + _productRepository = GetRequiredService>(); _testData = GetRequiredService(); } diff --git a/templates/module/app/MyCompanyName.MyProjectName.DemoApp/MyCompanyName.MyProjectName.DemoApp.csproj b/templates/module/app/MyCompanyName.MyProjectName.DemoApp/MyCompanyName.MyProjectName.DemoApp.csproj index 022b7607d2..4e019c6d9b 100644 --- a/templates/module/app/MyCompanyName.MyProjectName.DemoApp/MyCompanyName.MyProjectName.DemoApp.csproj +++ b/templates/module/app/MyCompanyName.MyProjectName.DemoApp/MyCompanyName.MyProjectName.DemoApp.csproj @@ -35,8 +35,4 @@ - - - - diff --git a/templates/module/app/MyCompanyName.MyProjectName.DemoApp/Startup.cs b/templates/module/app/MyCompanyName.MyProjectName.DemoApp/Startup.cs index d77c77ba38..18fac8ac52 100644 --- a/templates/module/app/MyCompanyName.MyProjectName.DemoApp/Startup.cs +++ b/templates/module/app/MyCompanyName.MyProjectName.DemoApp/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 MyCompanyName.MyProjectName.DemoApp @@ -22,15 +21,6 @@ namespace MyCompanyName.MyProjectName.DemoApp 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(); } } diff --git a/templates/mvc/src/MyCompanyName.MyProjectName.Domain/MyProjectNameDomainModule.cs b/templates/mvc/src/MyCompanyName.MyProjectName.Domain/MyProjectNameDomainModule.cs index 591780ab16..f32225025f 100644 --- a/templates/mvc/src/MyCompanyName.MyProjectName.Domain/MyProjectNameDomainModule.cs +++ b/templates/mvc/src/MyCompanyName.MyProjectName.Domain/MyProjectNameDomainModule.cs @@ -26,7 +26,7 @@ namespace MyCompanyName.MyProjectName { Configure(options => { - options.FileSets.AddEmbedded(); + options.FileSets.AddEmbedded("MyCompanyName.MyProjectName"); }); Configure(options => diff --git a/templates/mvc/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj b/templates/mvc/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj index f84de6cd67..debec3acbe 100644 --- a/templates/mvc/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj +++ b/templates/mvc/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj @@ -5,10 +5,6 @@ MyCompanyName.MyProjectName - - - - diff --git a/templates/mvc/src/MyCompanyName.MyProjectName.Web/Startup.cs b/templates/mvc/src/MyCompanyName.MyProjectName.Web/Startup.cs index 07f329bd59..2e788dc051 100644 --- a/templates/mvc/src/MyCompanyName.MyProjectName.Web/Startup.cs +++ b/templates/mvc/src/MyCompanyName.MyProjectName.Web/Startup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Serilog; using Volo.Abp; namespace MyCompanyName.MyProjectName @@ -21,15 +20,6 @@ namespace MyCompanyName.MyProjectName public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { - loggerFactory - .AddConsole() - .AddDebug() - .AddSerilog(new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.File("Logs/logs.txt") - .CreateLogger() - ); - app.InitializeApplication(); } } diff --git a/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190211105121_Initial.Designer.cs b/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190214093219_Initial.Designer.cs similarity index 98% rename from templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190211105121_Initial.Designer.cs rename to templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190214093219_Initial.Designer.cs index 61c15431db..ee67505ed5 100644 --- a/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190211105121_Initial.Designer.cs +++ b/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190214093219_Initial.Designer.cs @@ -10,14 +10,14 @@ using MyCompanyName.MyProjectName.Host; namespace MyCompanyName.MyProjectName.Host.Migrations { [DbContext(typeof(DemoAppDbContext))] - [Migration("20190211105121_Initial")] + [Migration("20190214093219_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.1-servicing-10028") + .HasAnnotation("ProductVersion", "2.2.0-rtm-35687") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); diff --git a/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190211105121_Initial.cs b/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190214093219_Initial.cs similarity index 100% rename from templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190211105121_Initial.cs rename to templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/20190214093219_Initial.cs diff --git a/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/DemoAppDbContextModelSnapshot.cs b/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/DemoAppDbContextModelSnapshot.cs index 5261599df5..ffe92c9c59 100644 --- a/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/DemoAppDbContextModelSnapshot.cs +++ b/templates/service/host/MyCompanyName.MyProjectName.Host/Migrations/DemoAppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace MyCompanyName.MyProjectName.Host.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.1-servicing-10028") + .HasAnnotation("ProductVersion", "2.2.0-rtm-35687") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); diff --git a/templates/service/host/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj b/templates/service/host/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj index ca352a43b3..f8b368d460 100644 --- a/templates/service/host/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj +++ b/templates/service/host/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj @@ -30,8 +30,4 @@ - - - -