├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── arch.drawio ├── building_blocks.png ├── clean_arch.png └── project_structure.png ├── components ├── appconfig.yaml ├── pubsub.yaml ├── statestore.yaml └── zipkin.yaml ├── global.json ├── modernstore.sln ├── modernstore.sln.DotSettings ├── nuget.config ├── restclient.http ├── src ├── BuildingBlocks │ ├── N8T.Domain │ │ ├── CoreException.cs │ │ ├── DomainEventBase.cs │ │ ├── EntityBase.cs │ │ ├── IAggregateRoot.cs │ │ └── N8T.Domain.csproj │ ├── N8T.Infrastructure.Cache │ │ ├── Extensions.cs │ │ ├── IRedisCacheService.cs │ │ ├── N8T.Infrastructure.Cache.csproj │ │ ├── RedisCacheOptions.cs │ │ └── RedisCacheService.cs │ ├── N8T.Infrastructure.Dapper │ │ ├── Extensions.cs │ │ ├── N8T.Infrastructure.Dapper.csproj │ │ ├── SimpleCRUD.cs │ │ └── SimpleCRUDAsync.cs │ ├── N8T.Infrastructure.EfCore │ │ ├── AppDbContextBase.cs │ │ ├── DbContextDesignFactoryBase.cs │ │ ├── DbContextMigratorHostedService.cs │ │ ├── Extensions.cs │ │ ├── IDbFacadeResolver.cs │ │ ├── ITxRequest.cs │ │ ├── N8T.Infrastructure.EfCore.csproj │ │ └── TxBehavior.cs │ ├── N8T.Infrastructure.GraphQL │ │ ├── Errors │ │ │ └── ValidationErrorFilter.cs │ │ ├── Extensions.cs │ │ ├── GraphQueryBase.cs │ │ ├── N8T.Infrastructure.GraphQL.csproj │ │ ├── OffsetPaging │ │ │ ├── OffsetPaging.cs │ │ │ └── OffsetPagingType.cs │ │ └── ProtoObjectType.cs │ ├── N8T.Infrastructure.OTel │ │ ├── Extensions.cs │ │ ├── MediatR │ │ │ ├── OTelMediatRDiagnosticListener.cs │ │ │ ├── OTelMediatRInstrumentation.cs │ │ │ ├── OTelMediatROptions.cs │ │ │ └── OTelMediatRTracingBehavior.cs │ │ └── N8T.Infrastructure.OTel.csproj │ └── N8T.Infrastructure │ │ ├── AppOptions.cs │ │ ├── Auth │ │ ├── AuthBehavior.cs │ │ ├── Extensions.cs │ │ └── IAuthRequest.cs │ │ ├── Consts.cs │ │ ├── Dapr │ │ └── Extensions.cs │ │ ├── Extensions.cs │ │ ├── Grpc │ │ ├── ExceptionHandleInterceptor.cs │ │ └── Extensions.cs │ │ ├── Helpers │ │ ├── ConfigurationHelper.cs │ │ └── DateTimeHelper.cs │ │ ├── Kestrel │ │ └── Extensions.cs │ │ ├── Linq │ │ └── Extensions.cs │ │ ├── Logging │ │ └── LoggingBehavior.cs │ │ ├── N8T.Infrastructure.csproj │ │ ├── Status │ │ ├── Extensions.cs │ │ └── StatusModel.cs │ │ ├── Tye │ │ └── Extensions.cs │ │ └── Validator │ │ ├── Extensions.cs │ │ ├── RequestValidationBehavior.cs │ │ ├── ValidationError.cs │ │ ├── ValidationException.cs │ │ └── ValidationResultModel.cs ├── Gateway │ ├── AppGateway.csproj │ ├── InMemoryConfigProvider.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── IdentityService │ ├── Config.cs │ ├── Custom │ │ └── ProfileService.cs │ ├── IdentityService.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Quickstart │ │ ├── Account │ │ │ ├── AccountController.cs │ │ │ ├── AccountOptions.cs │ │ │ ├── ExternalController.cs │ │ │ ├── ExternalProvider.cs │ │ │ ├── LoggedOutViewModel.cs │ │ │ ├── LoginInputModel.cs │ │ │ ├── LoginViewModel.cs │ │ │ ├── LogoutInputModel.cs │ │ │ ├── LogoutViewModel.cs │ │ │ └── RedirectViewModel.cs │ │ ├── Consent │ │ │ ├── ConsentController.cs │ │ │ ├── ConsentInputModel.cs │ │ │ ├── ConsentOptions.cs │ │ │ ├── ConsentViewModel.cs │ │ │ ├── ProcessConsentResult.cs │ │ │ └── ScopeViewModel.cs │ │ ├── Device │ │ │ ├── DeviceAuthorizationInputModel.cs │ │ │ ├── DeviceAuthorizationViewModel.cs │ │ │ └── DeviceController.cs │ │ ├── Diagnostics │ │ │ ├── DiagnosticsController.cs │ │ │ └── DiagnosticsViewModel.cs │ │ ├── Extensions.cs │ │ ├── Grants │ │ │ ├── GrantsController.cs │ │ │ └── GrantsViewModel.cs │ │ ├── Home │ │ │ ├── ErrorViewModel.cs │ │ │ └── HomeController.cs │ │ ├── SecurityHeadersAttribute.cs │ │ └── TestUsers.cs │ ├── Startup.cs │ ├── Views │ │ ├── Account │ │ │ ├── AccessDenied.cshtml │ │ │ ├── LoggedOut.cshtml │ │ │ ├── Login.cshtml │ │ │ └── Logout.cshtml │ │ ├── Consent │ │ │ └── Index.cshtml │ │ ├── Device │ │ │ ├── Success.cshtml │ │ │ ├── UserCodeCapture.cshtml │ │ │ └── UserCodeConfirmation.cshtml │ │ ├── Diagnostics │ │ │ └── Index.cshtml │ │ ├── Grants │ │ │ └── Index.cshtml │ │ ├── Home │ │ │ └── Index.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ ├── Redirect.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _Nav.cshtml │ │ │ ├── _ScopeListItem.cshtml │ │ │ └── _ValidationSummary.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── tempkey.jwk │ ├── updateUI.ps1 │ └── wwwroot │ │ ├── css │ │ ├── site.css │ │ ├── site.min.css │ │ └── site.scss │ │ ├── favicon.ico │ │ ├── icon.jpg │ │ ├── icon.png │ │ ├── js │ │ ├── signin-redirect.js │ │ └── signout-redirect.js │ │ └── lib │ │ ├── bootstrap │ │ ├── README.md │ │ ├── dist │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ └── scss │ │ │ ├── _alert.scss │ │ │ ├── _badge.scss │ │ │ ├── _breadcrumb.scss │ │ │ ├── _button-group.scss │ │ │ ├── _buttons.scss │ │ │ ├── _card.scss │ │ │ ├── _carousel.scss │ │ │ ├── _close.scss │ │ │ ├── _code.scss │ │ │ ├── _custom-forms.scss │ │ │ ├── _dropdown.scss │ │ │ ├── _forms.scss │ │ │ ├── _functions.scss │ │ │ ├── _grid.scss │ │ │ ├── _images.scss │ │ │ ├── _input-group.scss │ │ │ ├── _jumbotron.scss │ │ │ ├── _list-group.scss │ │ │ ├── _media.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modal.scss │ │ │ ├── _nav.scss │ │ │ ├── _navbar.scss │ │ │ ├── _pagination.scss │ │ │ ├── _popover.scss │ │ │ ├── _print.scss │ │ │ ├── _progress.scss │ │ │ ├── _reboot.scss │ │ │ ├── _root.scss │ │ │ ├── _spinners.scss │ │ │ ├── _tables.scss │ │ │ ├── _toasts.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _transitions.scss │ │ │ ├── _type.scss │ │ │ ├── _utilities.scss │ │ │ ├── _variables.scss │ │ │ ├── bootstrap-grid.scss │ │ │ ├── bootstrap-reboot.scss │ │ │ ├── bootstrap.scss │ │ │ ├── mixins │ │ │ ├── _alert.scss │ │ │ ├── _background-variant.scss │ │ │ ├── _badge.scss │ │ │ ├── _border-radius.scss │ │ │ ├── _box-shadow.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _buttons.scss │ │ │ ├── _caret.scss │ │ │ ├── _clearfix.scss │ │ │ ├── _deprecate.scss │ │ │ ├── _float.scss │ │ │ ├── _forms.scss │ │ │ ├── _gradients.scss │ │ │ ├── _grid-framework.scss │ │ │ ├── _grid.scss │ │ │ ├── _hover.scss │ │ │ ├── _image.scss │ │ │ ├── _list-group.scss │ │ │ ├── _lists.scss │ │ │ ├── _nav-divider.scss │ │ │ ├── _pagination.scss │ │ │ ├── _reset-text.scss │ │ │ ├── _resize.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _size.scss │ │ │ ├── _table-row.scss │ │ │ ├── _text-emphasis.scss │ │ │ ├── _text-hide.scss │ │ │ ├── _text-truncate.scss │ │ │ ├── _transition.scss │ │ │ └── _visibility.scss │ │ │ ├── utilities │ │ │ ├── _align.scss │ │ │ ├── _background.scss │ │ │ ├── _borders.scss │ │ │ ├── _clearfix.scss │ │ │ ├── _display.scss │ │ │ ├── _embed.scss │ │ │ ├── _flex.scss │ │ │ ├── _float.scss │ │ │ ├── _overflow.scss │ │ │ ├── _position.scss │ │ │ ├── _screenreaders.scss │ │ │ ├── _shadows.scss │ │ │ ├── _sizing.scss │ │ │ ├── _spacing.scss │ │ │ ├── _stretched-link.scss │ │ │ ├── _text.scss │ │ │ └── _visibility.scss │ │ │ └── vendor │ │ │ └── _rfs.scss │ │ └── jquery │ │ ├── LICENSE.txt │ │ ├── README.md │ │ └── dist │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ ├── jquery.min.map │ │ ├── jquery.slim.js │ │ ├── jquery.slim.min.js │ │ └── jquery.slim.min.map ├── ProductionService │ ├── Core │ │ ├── Application │ │ │ ├── Common │ │ │ │ ├── BrandDto.cs │ │ │ │ ├── CategoryDto.cs │ │ │ │ └── ProductDto.cs │ │ │ ├── CreateProduct │ │ │ │ ├── CreateProductCommand.cs │ │ │ │ └── CreateProductHandler.cs │ │ │ ├── GetCategories │ │ │ │ ├── GetCategoriesHandler.cs │ │ │ │ └── GetCategoriesQuery.cs │ │ │ └── GetProducts │ │ │ │ ├── GetProductsHandler.cs │ │ │ │ └── GetProductsQuery.cs │ │ ├── Domain │ │ │ ├── Event │ │ │ │ └── ProductCreated.cs │ │ │ ├── Exception │ │ │ │ ├── NotFoundBrandException.cs │ │ │ │ └── NotFoundCategoryException.cs │ │ │ └── Model │ │ │ │ ├── Brand.cs │ │ │ │ ├── Category.cs │ │ │ │ ├── Product.cs │ │ │ │ ├── Stock.cs │ │ │ │ └── Store.cs │ │ └── Infrastructure │ │ │ ├── CacheKeys.cs │ │ │ └── Persistence │ │ │ ├── MainDbContext.cs │ │ │ ├── Migrations │ │ │ ├── 20200920093728_InitialProductionDb.Designer.cs │ │ │ ├── 20200920093728_InitialProductionDb.cs │ │ │ ├── 20200922160111_InitSeedData.Designer.cs │ │ │ ├── 20200922160111_InitSeedData.cs │ │ │ └── MainDbContextModelSnapshot.cs │ │ │ └── Scripts │ │ │ └── 20200922160111_load-production.sql │ ├── Http │ │ └── Controllers │ │ │ ├── CategoryController.cs │ │ │ ├── InfoController.cs │ │ │ └── ProductController.cs │ ├── ProductionService.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Publishers │ │ └── ProductPublisher.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── readme.txt └── SaleService │ ├── Core │ ├── Application │ │ └── SyncProductInfo │ │ │ ├── SyncProductInfoCommand.cs │ │ │ └── SyncProductInfoHandler.cs │ ├── Domain │ │ ├── Customer.cs │ │ ├── Order.cs │ │ ├── OrderItem.cs │ │ ├── Product.cs │ │ ├── Staff.cs │ │ └── Store.cs │ └── Infrastructure │ │ └── Persistence │ │ ├── MainDbContext.cs │ │ ├── Migrations │ │ ├── 20201002075638_InitialSaleDb.Designer.cs │ │ ├── 20201002075638_InitialSaleDb.cs │ │ ├── 20201002075758_InitialSeedData.Designer.cs │ │ ├── 20201002075758_InitialSeedData.cs │ │ └── MainDbContextModelSnapshot.cs │ │ └── Scripts │ │ └── 20201002075758_load-sale.sql │ ├── Http │ ├── WeatherForecast.cs │ └── WeatherForecastController.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── SaleService.csproj │ ├── Startup.cs │ ├── Subscribers │ └── SubscribeController.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── readme.txt └── tye.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | 27 | .tye 28 | .devcontainer/certs 29 | .logs 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cloud Native .NET Core 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modernstore project 2 | 3 | A store application uses modern technologies such as Dapr, OSM, Tye... 4 | 5 | # Get starting 6 | 7 | > [dotnet](dot.net) --version 5.0.100-rc.1.20452.10 8 | 9 | > [tye](https://github.com/dotnet/tye) --version 0.5.0-alpha.20468.1+3402fbddeea6a31310c181b48a6281f84865aabc 10 | 11 | > [dapr](https://github.com/dapr/dapr) --version CLI version: 0.10.0 Runtime version: 0.10.0 12 | 13 | > [osm](https://github.com/openservicemesh/osm) version Version: v0.3.0; Commit: c91c782; Date: 2020-08-12-21:49 14 | 15 | ## Step 1: 16 | 17 | ```bash 18 | $ tye run 19 | ``` 20 | 21 | ## Step 2: 22 | 23 | Go to `http://localhost:8000` to find the appropriate port for `production-service` 24 | 25 | ## Step 3: 26 | 27 | Then we can use `restclient.http` to test the application. Happy hacking! 28 | 29 | # Clean Domain-driven Architecture 30 | 31 | ![](assets/clean_arch.png) 32 | 33 | # [Rest Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) 34 | 35 | Edit and add content to `settings.json` file: 36 | 37 | ```json 38 | "rest-client.environmentVariables": { 39 | "$shared": { 40 | "contentType": "application/json", 41 | "idphost": "http://:", 42 | "host": "http://:", 43 | } 44 | } 45 | ``` 46 | 47 | # Database schema 48 | 49 | The database schema is inspired from sqlservertutorial project at https://www.sqlservertutorial.net/sql-server-sample-database/ 50 | 51 | # References 52 | 53 | - https://techcommunity.microsoft.com/t5/azure-sql-database/10k-request-per-second-rest-api-with-azure-sql-dapper-and-json/ba-p/1189675 -------------------------------------------------------------------------------- /assets/building_blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/assets/building_blocks.png -------------------------------------------------------------------------------- /assets/clean_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/assets/clean_arch.png -------------------------------------------------------------------------------- /assets/project_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/assets/project_structure.png -------------------------------------------------------------------------------- /components/appconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Configuration 3 | metadata: 4 | name: appconfig 5 | spec: 6 | tracing: 7 | enabled: true 8 | expandParams: true 9 | includeBody: true 10 | -------------------------------------------------------------------------------- /components/pubsub.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: pubsub 5 | spec: 6 | type: pubsub.redis 7 | metadata: 8 | - name: redisHost 9 | value: localhost:6379 10 | - name: redisPassword 11 | value: "" 12 | -------------------------------------------------------------------------------- /components/statestore.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: statestore 5 | spec: 6 | type: state.redis 7 | metadata: 8 | - name: redisHost 9 | value: localhost:6379 10 | - name: redisPassword 11 | value: "" 12 | - name: actorStateStore 13 | value: "false" 14 | -------------------------------------------------------------------------------- /components/zipkin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: dapr.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: zipkin 5 | spec: 6 | type: exporters.zipkin 7 | metadata: 8 | - name: enabled 9 | value: "true" 10 | - name: exporterAddress 11 | value: "http://localhost:9411/api/v2/spans" 12 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.100-rc.1.20452.10" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /modernstore.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True 9 | True 10 | 11 | True 12 | True 13 | True 14 | True 15 | True 16 | True 17 | True -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /restclient.http: -------------------------------------------------------------------------------- 1 | ### 2 | # @name auth 3 | POST {{idphost}}/connect/token HTTP/1.1 4 | content-type: application/x-www-form-urlencoded 5 | 6 | grant_type=password 7 | &client_id=store.password 8 | &client_secret=49C1A7E1-0C79-4A89-A3D6-A37998FB86B0 9 | &username=bob 10 | &password=bob 11 | 12 | ### 13 | @term = Fashion 14 | @page = 1 15 | @pageSize = 20 16 | GET {{host}}/products?term={{term}}&page={{page}}&pageSize={{pageSize}} HTTP/1.1 17 | content-type: {{contentType}} 18 | 19 | ### 20 | POST {{host}}/products HTTP/1.1 21 | content-type: {{contentType}} 22 | authorization: bearer {{auth.response.body.access_token}} 23 | 24 | { 25 | "name": "sample 02", 26 | "modelYear": 2020, 27 | "listPrice": 1000, 28 | "brandId": 1, 29 | "categoryId": 1 30 | } 31 | 32 | ### 33 | GET {{host}}/categories HTTP/1.1 34 | content-type: {{contentType}} 35 | authorization: bearer {{auth.response.body.access_token}} 36 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Domain/CoreException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace N8T.Domain 4 | { 5 | public class CoreException : Exception 6 | { 7 | public CoreException(string message) : base(message) 8 | { 9 | } 10 | 11 | public static CoreException Exception(string message) 12 | { 13 | return new CoreException(message); 14 | } 15 | 16 | public static CoreException NullArgument(string arg) 17 | { 18 | return new CoreException($"{arg} cannot be null"); 19 | } 20 | 21 | public static CoreException InvalidArgument(string arg) 22 | { 23 | return new CoreException($"{arg} is invalid"); 24 | } 25 | 26 | public static CoreException NotFound(string arg) 27 | { 28 | return new CoreException($"{arg} was not found"); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Domain/DomainEventBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MediatR; 4 | 5 | namespace N8T.Domain 6 | { 7 | public interface IDomainEvent : INotification 8 | { 9 | DateTime CreatedAt { get; } 10 | IDictionary MetaData { get; } 11 | } 12 | 13 | public interface IDomainEventContext 14 | { 15 | IEnumerable GetDomainEvents(); 16 | } 17 | 18 | public abstract class DomainEventBase : IDomainEvent 19 | { 20 | public DateTime CreatedAt { get; } = DateTime.UtcNow; 21 | public IDictionary MetaData { get; } = new Dictionary(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Domain/EntityBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace N8T.Domain 5 | { 6 | public abstract class EntityBase 7 | { 8 | public DateTime Created { get; protected set; } = DateTime.UtcNow; 9 | public DateTime? Updated { get; protected set; } 10 | public HashSet DomainEvents { get; private set; } 11 | 12 | public void AddDomainEvent(DomainEventBase eventItem) 13 | { 14 | DomainEvents ??= new HashSet(); 15 | DomainEvents.Add(eventItem); 16 | } 17 | 18 | public void RemoveDomainEvent(DomainEventBase eventItem) 19 | { 20 | DomainEvents?.Remove(eventItem); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Domain/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Domain 2 | { 3 | public interface IAggregateRoot 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Domain/N8T.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.Cache/IRedisCacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace N8T.Infrastructure.Cache 6 | { 7 | public interface IRedisCacheService 8 | { 9 | Task GetOrSetAsync(string key, Func> func); 10 | Task GetOrSetAsync(string key, Func> func, TimeSpan expiration); 11 | Task HashGetOrSetAsync(string key, string hashField, Func> func); 12 | Task> GetKeysAsync(string pattern); 13 | Task RemoveAllKeysAsync(string pattern = "*"); 14 | Task RemoveAsync(string key); 15 | Task ResetAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.Cache/N8T.Infrastructure.Cache.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.Cache/RedisCacheOptions.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure.Cache 2 | { 3 | public class RedisCacheOptions 4 | { 5 | public string Url { get; set; } = "localhost:6379"; 6 | public string Prefix { get; set; } = ""; 7 | public string Password { get; set; } = ""; 8 | public int RedisDefaultSlidingExpirationInSecond { get; set; } = 3600; 9 | public int ConnectRetry { get; set; } = 5; 10 | public bool AbortOnConnectFail { get; set; } = false; 11 | public int ConnectTimeout { get; set; } = 5000; 12 | public int SyncTimeout { get; set; } = 5000; 13 | public int DeltaBackoffMiliseconds { get; set; } = 10000; 14 | public bool Ssl { get; set; } = false; 15 | 16 | public string GetConnectionString() 17 | { 18 | return string.IsNullOrEmpty(Password) ? Url : $"{Url},password={Password},ssl={Ssl},abortConnect=False"; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.Dapper/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using Dapper; 7 | 8 | namespace N8T.Infrastructure.Dapper 9 | { 10 | public static class Extensions 11 | { 12 | /// 13 | /// this is sample for how to use DataReader 14 | /// var reader = await _connection.ExecuteReaderAsync(query, @params); 15 | /// var result = reader.GetData>(); 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | public static T GetData(this IDataReader reader, int ordinal = 0) 22 | { 23 | if (reader == null) throw new ArgumentNullException(nameof(reader)); 24 | if (ordinal < 0) throw new ArgumentOutOfRangeException(nameof(ordinal)); 25 | 26 | var builder = new StringBuilder(); 27 | 28 | while (reader.Read()) 29 | { 30 | // only use for json string return from database with "FOR JSON PATH" 31 | builder.Append(reader.GetString(ordinal)); 32 | } 33 | 34 | return string.IsNullOrEmpty(builder.ToString()) 35 | ? default 36 | : JsonSerializer.Deserialize(builder.ToString()); 37 | } 38 | 39 | public static async Task QueryData(this IDbConnection connection, string sql, object param = null, 40 | IDbTransaction transaction = null, int? cmdTimeout = null, CommandType? cmdType = null) 41 | { 42 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 43 | if (sql == null) throw new ArgumentNullException(nameof(sql)); 44 | 45 | var builder = new StringBuilder(); 46 | 47 | // only use for json string return from database with "FOR JSON PATH" 48 | foreach (var s in await connection.QueryAsync(sql, param, transaction, cmdTimeout, cmdType)) 49 | { 50 | builder.Append(s); 51 | } 52 | 53 | return string.IsNullOrEmpty(builder.ToString()) 54 | ? default 55 | : JsonSerializer.Deserialize(builder.ToString()); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.Dapper/N8T.Infrastructure.Dapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/AppDbContextBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.EntityFrameworkCore; 4 | using N8T.Domain; 5 | 6 | namespace N8T.Infrastructure.EfCore 7 | { 8 | public abstract class AppDbContextBase : DbContext, IDomainEventContext, IDbFacadeResolver 9 | { 10 | protected AppDbContextBase(DbContextOptions options) : base(options) 11 | { 12 | } 13 | 14 | public IEnumerable GetDomainEvents() 15 | { 16 | var domainEntities = ChangeTracker 17 | .Entries() 18 | .Where(x => 19 | x.Entity.DomainEvents != null && 20 | x.Entity.DomainEvents.Any()) 21 | .ToList(); 22 | 23 | var domainEvents = domainEntities 24 | .SelectMany(x => x.Entity.DomainEvents) 25 | .ToList(); 26 | 27 | domainEntities.ForEach(entity => entity.Entity.DomainEvents.Clear()); 28 | 29 | return domainEvents; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/DbContextDesignFactoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Design; 4 | using Microsoft.Extensions.Configuration; 5 | using N8T.Infrastructure.Helpers; 6 | 7 | namespace N8T.Infrastructure.EfCore 8 | { 9 | public abstract class DbContextDesignFactoryBase : IDesignTimeDbContextFactory 10 | where TDbContext : DbContext 11 | { 12 | public TDbContext CreateDbContext(string[] args) 13 | { 14 | var connString = ConfigurationHelper.GetConfiguration(AppContext.BaseDirectory) 15 | ?.GetConnectionString("sqlserver"); 16 | 17 | Console.WriteLine($"Connection String: {connString}"); 18 | 19 | var optionsBuilder = new DbContextOptionsBuilder() 20 | .UseSqlServer( 21 | connString ?? throw new InvalidOperationException(), 22 | sqlOptions => 23 | { 24 | sqlOptions.MigrationsAssembly(GetType().Assembly.FullName); 25 | sqlOptions.EnableRetryOnFailure(15, TimeSpan.FromSeconds(30), null); 26 | } 27 | ); 28 | 29 | Console.WriteLine(connString); 30 | return (TDbContext)Activator.CreateInstance(typeof(TDbContext), optionsBuilder.Options); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/DbContextMigratorHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Data.SqlClient; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Polly; 10 | using Polly.Retry; 11 | 12 | namespace N8T.Infrastructure.EfCore 13 | { 14 | public class DbContextMigratorHostedService : IHostedService 15 | { 16 | private readonly IServiceProvider _serviceProvider; 17 | private readonly ILogger _logger; 18 | 19 | public DbContextMigratorHostedService(IServiceProvider serviceProvider, 20 | ILogger logger) 21 | { 22 | _serviceProvider = serviceProvider; 23 | _logger = logger; 24 | } 25 | 26 | public async Task StartAsync(CancellationToken cancellationToken) 27 | { 28 | var policy = CreatePolicy(3, _logger, nameof(DbContextMigratorHostedService)); 29 | await policy.ExecuteAsync(async () => 30 | { 31 | using var scope = _serviceProvider.CreateScope(); 32 | var dbFacadeResolver = scope.ServiceProvider.GetRequiredService(); 33 | await dbFacadeResolver.Database.MigrateAsync(cancellationToken); 34 | _logger.LogInformation("Done migration database schema."); 35 | }); 36 | } 37 | 38 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 39 | 40 | private static AsyncRetryPolicy CreatePolicy(int retries, ILogger logger, string prefix) 41 | { 42 | return Policy.Handle().WaitAndRetryAsync( 43 | retryCount: retries, 44 | sleepDurationProvider: retry => TimeSpan.FromSeconds(5), 45 | onRetry: (exception, timeSpan, retry, ctx) => 46 | { 47 | logger.LogWarning(exception, 48 | "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", 49 | prefix, exception.GetType().Name, exception.Message, retry, retries); 50 | } 51 | ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/IDbFacadeResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Infrastructure; 2 | 3 | namespace N8T.Infrastructure.EfCore 4 | { 5 | public interface IDbFacadeResolver 6 | { 7 | DatabaseFacade Database { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/ITxRequest.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure.EfCore 2 | { 3 | public interface ITxRequest 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.EfCore/N8T.Infrastructure.EfCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/Errors/ValidationErrorFilter.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate; 2 | using N8T.Infrastructure.Validator; 3 | 4 | namespace N8T.Infrastructure.GraphQL.Errors 5 | { 6 | public class ValidationErrorFilter : IErrorFilter 7 | { 8 | public IError OnError(IError error) 9 | { 10 | if (error.Exception is ValidationException exception) 11 | { 12 | return error.AddExtension("ValidationError", 13 | exception.ValidationResultModel.ToString()); 14 | } 15 | 16 | return error; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/GraphQueryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Text.Json.Serialization; 4 | using HotChocolate.Types.Sorting; 5 | 6 | namespace N8T.Infrastructure.GraphQL 7 | { 8 | public abstract class GraphQueryBase 9 | { 10 | [JsonIgnore] public Expression> FilterExpr { get; set; } 11 | [JsonIgnore] public QueryableSortVisitor SortingVisitor { get; set; } 12 | public int Page { get; set; } = 1; 13 | public int PageSize { get; set; } = 20; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/N8T.Infrastructure.GraphQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/OffsetPaging/OffsetPaging.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N8T.Infrastructure.GraphQL.OffsetPaging 4 | { 5 | public class OffsetPaging 6 | { 7 | public IEnumerable Edges { get; set; } 8 | public long? TotalCount { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/OffsetPaging/OffsetPagingType.cs: -------------------------------------------------------------------------------- 1 | using HotChocolate.Types; 2 | 3 | namespace N8T.Infrastructure.GraphQL.OffsetPaging 4 | { 5 | public class OffsetPagingType : ObjectType> 6 | where TSchemaType : class, IOutputType 7 | where TValueType : new() 8 | { 9 | protected override void Configure(IObjectTypeDescriptor> descriptor) 10 | { 11 | descriptor.Field(t => t.Edges) 12 | .Name("edges") 13 | .Description("A list of edges.") 14 | .Type>(); 15 | 16 | descriptor.Field(t => t.TotalCount) 17 | .Type>(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.GraphQL/ProtoObjectType.cs: -------------------------------------------------------------------------------- 1 | using Google.Protobuf; 2 | using HotChocolate.Types; 3 | 4 | namespace N8T.Infrastructure.GraphQL 5 | { 6 | public class ProtoObjectType : ObjectType 7 | where T : IMessage 8 | { 9 | protected override void Configure(IObjectTypeDescriptor descriptor) 10 | { 11 | descriptor.Field(t => t.CalculateSize()).Ignore(); 12 | descriptor.Field(t => t.Clone()).Ignore(); 13 | descriptor.Field(t => t.Equals(default)).Ignore(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.OTel/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MediatR; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using N8T.Infrastructure.OTel.MediatR; 6 | using OpenTelemetry.Exporter.Zipkin; 7 | using OpenTelemetry.Trace; 8 | using OpenTelemetry.Trace.Samplers; 9 | 10 | namespace N8T.Infrastructure.OTel 11 | { 12 | public static class Extensions 13 | { 14 | public static IServiceCollection AddCustomOtelWithZipkin(this IServiceCollection services, 15 | IConfiguration config, Action configureZipkin = null) 16 | { 17 | services.AddOpenTelemetry(b => b 18 | .SetSampler(new AlwaysOnSampler()) 19 | .AddAspNetCoreInstrumentation() 20 | .AddHttpClientInstrumentation() 21 | .AddGrpcClientInstrumentation() 22 | .AddSqlClientDependencyInstrumentation() 23 | .AddMediatRInstrumentation() 24 | .UseZipkinExporter(o => 25 | { 26 | config.Bind("OtelZipkin", o); 27 | configureZipkin?.Invoke(o); 28 | }) 29 | ); 30 | 31 | services.AddScoped(typeof(IPipelineBehavior<,>), typeof(OTelMediatRTracingBehavior<,>)); 32 | 33 | return services; 34 | } 35 | 36 | public static TracerProviderBuilder AddMediatRInstrumentation( 37 | this TracerProviderBuilder builder) 38 | { 39 | if (builder == null) 40 | { 41 | throw new ArgumentNullException(nameof(builder)); 42 | } 43 | 44 | builder.AddInstrumentation((activitySource) => new OTelMediatRInstrumentation(activitySource)); 45 | builder.AddActivitySource(OTelMediatROptions.OTelMediatRName); 46 | 47 | return builder; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.OTel/MediatR/OTelMediatRDiagnosticListener.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json; 3 | using OpenTelemetry.Instrumentation; 4 | using OpenTelemetry.Trace; 5 | 6 | namespace N8T.Infrastructure.OTel.MediatR 7 | { 8 | public class OTelMediatRDiagnosticListener : ListenerHandler 9 | { 10 | private readonly ActivitySourceAdapter _activitySource; 11 | 12 | public OTelMediatRDiagnosticListener(string name, ActivitySourceAdapter activitySource) : base(name) 13 | { 14 | _activitySource = activitySource; 15 | } 16 | 17 | public override void OnStartActivity(Activity activity, object payload) 18 | { 19 | // add more tags 20 | activity.AddTag("request", activity.OperationName); 21 | activity.AddTag("request.data", JsonSerializer.Serialize(payload)); 22 | 23 | activity.SetKind(ActivityKind.Server); 24 | 25 | _activitySource.Start(activity); 26 | } 27 | 28 | public override void OnStopActivity(Activity activity, object payload) 29 | { 30 | _activitySource.Stop(activity); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.OTel/MediatR/OTelMediatRInstrumentation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenTelemetry.Instrumentation; 3 | using OpenTelemetry.Trace; 4 | 5 | namespace N8T.Infrastructure.OTel.MediatR 6 | { 7 | public class OTelMediatRInstrumentation : IDisposable 8 | { 9 | private readonly DiagnosticSourceSubscriber _diagnosticSourceSubscriber; 10 | 11 | public OTelMediatRInstrumentation(ActivitySourceAdapter activitySource) 12 | { 13 | if (activitySource == null) 14 | { 15 | throw new ArgumentNullException(nameof(activitySource)); 16 | } 17 | 18 | _diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( 19 | name => new OTelMediatRDiagnosticListener(OTelMediatROptions.OTelMediatRName, activitySource), 20 | listener => 21 | { 22 | return listener.Name.Contains(OTelMediatROptions.OTelMediatRName); 23 | }, 24 | null 25 | ); 26 | 27 | _diagnosticSourceSubscriber?.Subscribe(); 28 | } 29 | 30 | public void Dispose() 31 | { 32 | _diagnosticSourceSubscriber?.Dispose(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.OTel/MediatR/OTelMediatROptions.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure.OTel.MediatR 2 | { 3 | public class OTelMediatROptions 4 | { 5 | public static string OTelMediatRName = "OpenTelemetry.MediatR"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure.OTel/N8T.Infrastructure.OTel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/AppOptions.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure 2 | { 3 | public class AppOptions 4 | { 5 | public string Name { get; set; } = "Default App"; 6 | public NoTyeOptions NoTye { get; set; } = new NoTyeOptions(); 7 | } 8 | 9 | public class NoTyeOptions 10 | { 11 | public bool Enabled { get; set; } = false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Auth/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Claims; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace N8T.Infrastructure.Auth 10 | { 11 | public static class Extensions 12 | { 13 | public static IServiceCollection AddCustomAuth(this IServiceCollection services, 14 | IConfiguration config, 15 | Action configureOptions = null, 16 | Action configureMoreActions = null) 17 | { 18 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 19 | .AddJwtBearer(options => 20 | { 21 | config.Bind("Authn", options); 22 | configureOptions?.Invoke(options); 23 | }); 24 | 25 | services.AddAuthorization(options => 26 | { 27 | options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => 28 | { 29 | policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); 30 | policy.RequireClaim(ClaimTypes.Name); 31 | }); 32 | }); 33 | 34 | services.AddScoped(typeof(IPipelineBehavior<,>), typeof(AuthBehavior<,>)); 35 | 36 | services.Scan(s => s 37 | .FromAssemblyOf() 38 | .AddClasses(c => c 39 | .AssignableTo()).As() 40 | .AddClasses(c => c 41 | .AssignableTo()).As() 42 | ); 43 | 44 | configureMoreActions?.Invoke(services); 45 | 46 | return services; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Auth/IAuthRequest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace N8T.Infrastructure.Auth 4 | { 5 | public interface IAuthRequest : IAuthorizationRequirement 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Consts.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure 2 | { 3 | public class Consts 4 | { 5 | // graphql 6 | public const string PRODUCT_CATALOG_GRAPHQL_CLIENT = "ProductCatalog"; 7 | public const string INVENTORY_GRAPHQL_CLIENT = "Inventory"; 8 | public const string SHOPPING_CART_GRAPHQL_CLIENT = "ShoppingCart"; 9 | 10 | // dapr 11 | public const string SQLSERVER_DB_ID = "sqlserver"; 12 | public const string PRODUCT_CATALOG_API_ID = "productcatalogapi"; 13 | public const string INVENTORY_API_ID = "inventoryapi"; 14 | public const string WEBUI_ID = "webuihost"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Dapr/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace N8T.Infrastructure.Dapr 5 | { 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddCustomDaprClient(this IServiceCollection services) 9 | { 10 | var options = new JsonSerializerOptions() 11 | { 12 | PropertyNameCaseInsensitive = true, 13 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 14 | }; 15 | 16 | services.AddDaprClient(client => 17 | { 18 | client.UseJsonSerializationOptions(options); 19 | }); 20 | 21 | return services; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Grpc/ExceptionHandleInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Grpc.Core; 5 | using Grpc.Core.Interceptors; 6 | using N8T.Infrastructure.Validator; 7 | using Serilog; 8 | 9 | namespace N8T.Infrastructure.Grpc 10 | { 11 | public class ExceptionHandleInterceptor : Interceptor 12 | { 13 | private const string MessageTemplate = 14 | "{RequestMethod} responded {StatusCode} with error message {ErrorMessage}"; 15 | 16 | [DebuggerStepThrough] 17 | public override async Task UnaryServerHandler(TRequest request, 18 | ServerCallContext context, UnaryServerMethod continuation) 19 | { 20 | try 21 | { 22 | var response = await base.UnaryServerHandler(request, context, continuation); 23 | return response; 24 | } 25 | catch (ValidationException ex) 26 | { 27 | Log.Logger.Error(MessageTemplate, 28 | context.Method, 29 | context.Status.StatusCode, 30 | ex.ValidationResultModel.ToString()); 31 | 32 | throw new RpcException(new global::Grpc.Core.Status(StatusCode.Internal, ex.ValidationResultModel.ToString())); 33 | } 34 | catch (Exception ex) 35 | { 36 | Log.Logger.Error(MessageTemplate, 37 | context.Method, 38 | context.Status.StatusCode, 39 | ex.Message); 40 | 41 | throw new RpcException(new global::Grpc.Core.Status(StatusCode.Internal, ex.Message)); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Grpc/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using Google.Protobuf; 4 | using Google.Protobuf.WellKnownTypes; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace N8T.Infrastructure.Grpc 8 | { 9 | public static class Extensions 10 | { 11 | public static Any ConvertToAnyTypeAsync(this T data, JsonSerializerOptions options = null) 12 | { 13 | options ??= new JsonSerializerOptions 14 | { 15 | PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase 16 | }; 17 | 18 | var any = new Any(); 19 | if (data == null) 20 | return any; 21 | 22 | var bytes = JsonSerializer.SerializeToUtf8Bytes(data, options); 23 | any.Value = ByteString.CopyFrom(bytes); 24 | 25 | return any; 26 | } 27 | 28 | public static T ConvertFromAnyTypeAsync(this Any any, JsonSerializerOptions options = null) 29 | { 30 | options ??= new JsonSerializerOptions 31 | { 32 | PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase 33 | }; 34 | 35 | var utf8String = any.Value.ToStringUtf8(); 36 | return JsonSerializer.Deserialize(utf8String, options); 37 | } 38 | 39 | public static IServiceCollection AddCustomGrpc(this IServiceCollection services, 40 | Action doMoreActions = null) 41 | { 42 | services.AddGrpc(options => 43 | { 44 | options.Interceptors.Add(); 45 | options.EnableDetailedErrors = true; 46 | }); 47 | 48 | doMoreActions?.Invoke(services); 49 | 50 | return services; 51 | } 52 | 53 | public static IServiceCollection AddCustomGrpcClient(this IServiceCollection services, 54 | Action doMoreActions = null) 55 | { 56 | doMoreActions?.Invoke(services); 57 | 58 | return services; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Helpers/ConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace N8T.Infrastructure.Helpers 6 | { 7 | public static class ConfigurationHelper 8 | { 9 | public static IConfiguration GetConfiguration(string basePath = null) 10 | { 11 | basePath ??= Directory.GetCurrentDirectory(); 12 | var builder = new ConfigurationBuilder() 13 | .SetBasePath(basePath) 14 | .AddJsonFile("appsettings.json") 15 | .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true) 16 | .AddEnvironmentVariables(); 17 | 18 | return builder.Build(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Helpers/DateTimeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace N8T.Infrastructure.Helpers 5 | { 6 | public static class DateTimeHelper 7 | { 8 | [DebuggerStepThrough] 9 | public static DateTime NewDateTime() 10 | { 11 | return DateTimeOffset.Now.UtcDateTime; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Kestrel/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using Microsoft.AspNetCore.Server.Kestrel.Core; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace N8T.Infrastructure.Kestrel 7 | { 8 | public static class Extensions 9 | { 10 | public static KestrelServerOptions ListenHttpAndGrpcProtocols( 11 | this KestrelServerOptions options, 12 | IConfiguration config) 13 | { 14 | options.Limits.MinRequestBodyDataRate = null; 15 | 16 | // todo: do a better way, instead of based on PORT env variable (Tye generated) 17 | var ports = config.GetValue("PORT").Split(";"); 18 | if (ports.Length != 2) 19 | { 20 | throw new Exception("Wrong binding port and protocols"); 21 | } 22 | 23 | // rest 24 | options.Listen(IPAddress.Any, ports[0].ConvertTo()); 25 | 26 | // grpc 27 | options.Listen(IPAddress.Any, 28 | ports[1].ConvertTo(), 29 | listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; }); 30 | 31 | return options; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Linq/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace N8T.Infrastructure.Linq 7 | { 8 | /// 9 | /// Copy from https://github.com/arpadbarta/PlatformExtensions 10 | /// 11 | public static class Extensions 12 | { 13 | public static void For(this IEnumerable enumerable, Action action) 14 | { 15 | var collection = enumerable.ToArray(); 16 | 17 | for (var i = 0; i < collection.Count(); i++) 18 | { 19 | action(i, collection[i]); 20 | } 21 | } 22 | 23 | public static IEnumerable SelectWithIndex(this IEnumerable enumerable, Func function) 24 | { 25 | var collection = enumerable.ToArray(); 26 | 27 | for (var i = 0; i < collection.Count(); i++) 28 | { 29 | yield return function(i, collection[i]); 30 | } 31 | } 32 | 33 | public static void Foreach(this IEnumerable enumerable, Action action) 34 | { 35 | foreach (var result in enumerable) 36 | { 37 | action(result); 38 | } 39 | } 40 | 41 | 42 | public static IEnumerable SelectTypeOf(this IEnumerable enumerable) 43 | { 44 | foreach (var element in enumerable) 45 | { 46 | if (element is TR typedElement) 47 | { 48 | yield return typedElement; 49 | } 50 | } 51 | } 52 | 53 | public static IEnumerable SelectTypeOf(this IEnumerable enumerable) 54 | { 55 | foreach (var element in enumerable) 56 | { 57 | if (element is TR typedElement) 58 | { 59 | yield return typedElement; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Logging/LoggingBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json; 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace N8T.Infrastructure.Logging 9 | { 10 | public class LoggingBehavior : IPipelineBehavior 11 | { 12 | private readonly ILogger> _logger; 13 | 14 | public LoggingBehavior(ILogger> logger) => _logger = logger; 15 | 16 | public async Task Handle(TRequest request, 17 | CancellationToken cancellationToken, 18 | RequestHandlerDelegate next) 19 | { 20 | _logger.LogInformation($"Handling {typeof(TRequest).FullName}"); 21 | _logger.LogDebug($"Handling {typeof(TRequest).FullName} with content {JsonSerializer.Serialize(request)}"); 22 | 23 | var timer = new Stopwatch(); 24 | timer.Start(); 25 | 26 | var response = await next(); 27 | 28 | timer.Stop(); 29 | var timeTaken = timer.Elapsed; 30 | if (timeTaken.Seconds > 3) // if the request is greater than 3 seconds, then log the warnings 31 | { 32 | _logger.LogWarning($"[PERF] The request {typeof(TRequest).FullName} took {timeTaken.Seconds} seconds."); 33 | } 34 | 35 | _logger.LogInformation($"Handled {typeof(TRequest).FullName}"); 36 | return response; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Status/StatusModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N8T.Infrastructure.Status 4 | { 5 | public class StatusModel 6 | { 7 | public string AppName { get; set; } 8 | public string AppVersion { get; set; } 9 | public string OSArchitecture { get; set; } 10 | public string OSDescription { get; set; } 11 | public string ProcessArchitecture { get; set; } 12 | public string BasePath { get; set; } 13 | public string RuntimeFramework { get; set; } 14 | public string FrameworkDescription { get; set; } 15 | public string HostName { get; set; } 16 | public string IPAddress { get; set; } 17 | public IDictionary Envs { get; set; } = new Dictionary(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Tye/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace N8T.Infrastructure.Tye 6 | { 7 | public static class Extensions 8 | { 9 | public static void AddTyeBindingSecrets(this IConfigurationBuilder config) 10 | { 11 | if (!Directory.Exists("/var/tye/bindings/")) 12 | return; 13 | foreach (var directory in Directory.GetDirectories("/var/tye/bindings/")) 14 | { 15 | Console.WriteLine($"Adding config in '{directory}'."); 16 | config.AddKeyPerFile(directory, true); 17 | } 18 | } 19 | 20 | public static Uri GetGraphQLUriFor(this IConfiguration config, string appId) 21 | { 22 | var appOptions = config.GetOptions("app"); 23 | 24 | var clientUrl = !appOptions.NoTye.Enabled 25 | ? config.GetServiceUri(appId)?.ToString() 26 | : config.GetValue($"app:noTye:services:{appId}:url"); 27 | 28 | return new Uri($"{clientUrl?.TrimEnd('/')}/graphql"); 29 | } 30 | 31 | public static Uri GetRestUriFor(this IConfiguration config, string appId) 32 | { 33 | var appOptions = config.GetOptions("app"); 34 | 35 | var clientUrl = !appOptions.NoTye.Enabled 36 | ? config.GetServiceUri(appId)?.ToString() 37 | : config.GetValue($"app:noTye:services:{appId}:url"); 38 | 39 | return new Uri(clientUrl?.TrimEnd('/')!); 40 | } 41 | 42 | public static Uri GetGrpcUriFor(this IConfiguration config, string appId) 43 | { 44 | var appOptions = config.GetOptions("app"); 45 | 46 | string clientUrl; 47 | if (!appOptions.NoTye.Enabled) 48 | { 49 | clientUrl = config 50 | .GetServiceUri(appId, "https") 51 | ?.ToString() 52 | .Replace("https", "http") /* hack: ssl termination */; 53 | } 54 | else 55 | { 56 | clientUrl = config.GetValue($"app:noTye:services:{appId}:grpcUrl"); 57 | } 58 | 59 | return new Uri(clientUrl!); 60 | } 61 | 62 | public static bool IsRunOnTye(this IConfiguration config, string serviceName) 63 | { 64 | return config.GetServiceUri(serviceName) is not null; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Validator/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using FluentValidation; 3 | using FluentValidation.Results; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace N8T.Infrastructure.Validator 7 | { 8 | public static class Extensions 9 | { 10 | private static ValidationResultModel ToValidationResultModel(this ValidationResult validationResult) 11 | { 12 | return new ValidationResultModel(validationResult); 13 | } 14 | 15 | /// 16 | /// Ref https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi 17 | /// 18 | public static async Task HandleValidation(this IValidator validator, TRequest request) 19 | { 20 | var validationResult = await validator.ValidateAsync(request); 21 | if (!validationResult.IsValid) 22 | { 23 | throw new ValidationException(validationResult.ToValidationResultModel()); 24 | } 25 | } 26 | 27 | public static IServiceCollection AddCustomValidators(this IServiceCollection services) 28 | { 29 | return services.Scan(scan => scan 30 | .FromAssemblyOf() 31 | .AddClasses(c => c.AssignableTo(typeof(IValidator<>))) 32 | .AsImplementedInterfaces() 33 | .WithTransientLifetime()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Validator/RequestValidationBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using FluentValidation; 6 | using MediatR; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace N8T.Infrastructure.Validator 10 | { 11 | public class RequestValidationBehavior : IPipelineBehavior 12 | where TRequest : IRequest 13 | { 14 | private readonly IValidator _validator; 15 | private readonly ILogger> _logger; 16 | 17 | public RequestValidationBehavior(IValidator validator, 18 | ILogger> logger) 19 | { 20 | _validator = validator ?? throw new ArgumentNullException(nameof(validator)); 21 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 22 | } 23 | 24 | public async Task Handle(TRequest request, 25 | CancellationToken cancellationToken, 26 | RequestHandlerDelegate next) 27 | { 28 | _logger.LogInformation($"Handling {typeof(TRequest).FullName}"); 29 | _logger.LogDebug($"Handling {typeof(TRequest).FullName} with content {JsonSerializer.Serialize(request)}"); 30 | await _validator.HandleValidation(request); 31 | var response = await next(); 32 | _logger.LogInformation($"Handled {typeof(TRequest).FullName}"); 33 | return response; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Validator/ValidationError.cs: -------------------------------------------------------------------------------- 1 | namespace N8T.Infrastructure.Validator 2 | { 3 | public class ValidationError 4 | { 5 | public string Field { get; } 6 | 7 | public string Message { get; } 8 | 9 | public ValidationError(string field, string message) 10 | { 11 | Field = field != string.Empty ? field : null; 12 | Message = message; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Validator/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace N8T.Infrastructure.Validator 4 | { 5 | public class ValidationException : Exception 6 | { 7 | public ValidationException(ValidationResultModel validationResultModel) 8 | { 9 | ValidationResultModel = validationResultModel; 10 | } 11 | 12 | public ValidationResultModel ValidationResultModel { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/N8T.Infrastructure/Validator/ValidationResultModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Text.Json; 5 | using FluentValidation.Results; 6 | 7 | namespace N8T.Infrastructure.Validator 8 | { 9 | public class ValidationResultModel 10 | { 11 | public int StatusCode { get; set; } = (int) HttpStatusCode.BadRequest; 12 | public string Message { get; set; } = "Validation Failed."; 13 | 14 | public List Errors { get; } 15 | 16 | public ValidationResultModel(ValidationResult validationResult = null) 17 | { 18 | Errors = validationResult.Errors 19 | .Select(error => new ValidationError(error.PropertyName, error.ErrorMessage)) 20 | .ToList(); 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return JsonSerializer.Serialize(this); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Gateway/AppGateway.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | preview 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Gateway/InMemoryConfigProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Primitives; 5 | using Microsoft.ReverseProxy.Abstractions; 6 | using Microsoft.ReverseProxy.Service; 7 | 8 | namespace AppGateway 9 | { 10 | public static class InMemoryConfigProviderExtensions 11 | { 12 | public static IReverseProxyBuilder LoadFromMemory(this IReverseProxyBuilder builder, 13 | IReadOnlyList routes, IReadOnlyList clusters) 14 | { 15 | builder.Services.AddSingleton(new InMemoryConfigProvider(routes, clusters)); 16 | return builder; 17 | } 18 | } 19 | 20 | public class InMemoryConfigProvider : IProxyConfigProvider 21 | { 22 | private volatile InMemoryConfig _config; 23 | 24 | public InMemoryConfigProvider(IReadOnlyList routes, IReadOnlyList clusters) 25 | { 26 | _config = new InMemoryConfig(routes, clusters); 27 | } 28 | 29 | public IProxyConfig GetConfig() => _config; 30 | 31 | public void Update(IReadOnlyList routes, IReadOnlyList clusters) 32 | { 33 | var oldConfig = _config; 34 | _config = new InMemoryConfig(routes, clusters); 35 | oldConfig.SignalChange(); 36 | } 37 | 38 | private class InMemoryConfig : IProxyConfig 39 | { 40 | private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 41 | 42 | public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters) 43 | { 44 | Routes = routes; 45 | Clusters = clusters; 46 | ChangeToken = new CancellationChangeToken(_cts.Token); 47 | } 48 | 49 | public IReadOnlyList Routes { get; } 50 | 51 | public IReadOnlyList Clusters { get; } 52 | 53 | public IChangeToken ChangeToken { get; } 54 | 55 | internal void SignalChange() 56 | { 57 | _cts.Cancel(); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Gateway/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using N8T.Infrastructure; 5 | 6 | namespace AppGateway 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureWebHostDefaults(webBuilder => 18 | { 19 | webBuilder.ConfigureAppConfiguration(builder => 20 | { 21 | var config = builder.Build(); 22 | var appOptions = config.GetOptions("app"); 23 | Console.WriteLine($"Starting {appOptions.Name}"); 24 | }); 25 | 26 | webBuilder.UseStartup(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Gateway/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ProductionService": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "p/products", 7 | "applicationUrl": "http://localhost:5000", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Gateway/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Gateway/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "Name": "app-gateway" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "AllowedHosts": "*", 13 | "ReverseProxy": { 14 | "Clusters": { 15 | "production-svc-cluster": { 16 | "Destinations": { 17 | "production-svc-cluster/destination1": { 18 | "Address": "http://localhost:5002/" 19 | } 20 | } 21 | }, 22 | "sale-svc-cluster": { 23 | "Destinations": { 24 | "sale-svc-cluster/destination1": { 25 | "Address": "http://localhost:5003/" 26 | } 27 | } 28 | } 29 | }, 30 | "Routes": [ 31 | { 32 | "RouteId": "production", 33 | "ClusterId": "production-svc-cluster", 34 | "Match": { 35 | "path": "/{**catchall}" 36 | }, 37 | "Transforms": [ 38 | { 39 | "X-Forwarded": "proto,host,for,pathbase", 40 | "Append": "true", 41 | "Prefix": "X-Forwarded-" 42 | }, 43 | 44 | { "RequestHeadersCopy": "true" }, 45 | { "RequestHeaderOriginalHost": "true" } 46 | ] 47 | }, 48 | { 49 | "RouteId": "sale", 50 | "ClusterId": "sale-svc-cluster", 51 | "Match": { 52 | "Methods": [ "GET", "POST" ], 53 | "Path": "/s/products" 54 | }, 55 | "Transforms": [ 56 | { 57 | "X-Forwarded": "proto,host,for,pathbase", 58 | "Append": "true", 59 | "Prefix": "X-Forwarded-" 60 | }, 61 | 62 | { "PathSet": "/weatherforecast" }, 63 | 64 | { "RequestHeadersCopy": "true" }, 65 | { "RequestHeaderOriginalHost": "true" } 66 | ] 67 | } 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/IdentityService/Custom/ProfileService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Threading.Tasks; 3 | using IdentityServer4.Extensions; 4 | using IdentityServer4.Models; 5 | using IdentityServer4.Services; 6 | 7 | namespace IdentityService.Custom 8 | { 9 | public class ProfileService : IProfileService 10 | { 11 | public Task GetProfileDataAsync(ProfileDataRequestContext context) 12 | { 13 | var userId = context.Subject.GetSubjectId(); 14 | 15 | switch (userId) 16 | { 17 | case "88421113": // bob 18 | context.IssuedClaims.Add(new Claim("user_role", "sys_admin")); 19 | break; 20 | default: 21 | context.IssuedClaims.Add(new Claim("user_role", "normal_user")); 22 | break; 23 | } 24 | 25 | return Task.CompletedTask; 26 | } 27 | 28 | public Task IsActiveAsync(IsActiveContext context) 29 | { 30 | var sub = context.Subject.GetSubjectId(); 31 | context.IsActive = sub != null; 32 | 33 | return Task.CompletedTask; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/IdentityService/IdentityService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/IdentityService/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | using Serilog; 8 | using Serilog.Events; 9 | using Serilog.Sinks.SystemConsole.Themes; 10 | using System; 11 | 12 | namespace IdentityService 13 | { 14 | public class Program 15 | { 16 | public static int Main(string[] args) 17 | { 18 | Log.Logger = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 21 | .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) 22 | .MinimumLevel.Override("System", LogEventLevel.Warning) 23 | .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) 24 | .Enrich.FromLogContext() 25 | // uncomment to write to Azure diagnostics stream 26 | //.WriteTo.File( 27 | // @"D:\home\LogFiles\Application\identityserver.txt", 28 | // fileSizeLimitBytes: 1_000_000, 29 | // rollOnFileSizeLimit: true, 30 | // shared: true, 31 | // flushToDiskInterval: TimeSpan.FromSeconds(1)) 32 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) 33 | .CreateLogger(); 34 | 35 | try 36 | { 37 | Log.Information("Starting host..."); 38 | CreateHostBuilder(args).Build().Run(); 39 | return 0; 40 | } 41 | catch (Exception ex) 42 | { 43 | Log.Fatal(ex, "Host terminated unexpectedly."); 44 | return 1; 45 | } 46 | finally 47 | { 48 | Log.CloseAndFlush(); 49 | } 50 | } 51 | 52 | public static IHostBuilder CreateHostBuilder(string[] args) => 53 | Host.CreateDefaultBuilder(args) 54 | .UseSerilog() 55 | .ConfigureWebHostDefaults(webBuilder => 56 | { 57 | webBuilder.UseStartup(); 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /src/IdentityService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SelfHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5001" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class AccountOptions 10 | { 11 | public static bool AllowLocalLogin = true; 12 | public static bool AllowRememberLogin = true; 13 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 14 | 15 | public static bool ShowLogoutPrompt = true; 16 | public static bool AutomaticRedirectAfterSignOut = false; 17 | 18 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | 13 | public bool AutomaticRedirectAfterSignOut { get; set; } 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class LoginInputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | [Required] 14 | public string Password { get; set; } 15 | public bool RememberLogin { get; set; } 16 | public string ReturnUrl { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace IdentityServerHost.Quickstart.UI 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } = true; 14 | public bool EnableLocalLogin { get; set; } = true; 15 | 16 | public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); 17 | public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 18 | 19 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 20 | public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 21 | } 22 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } = true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Account/RedirectViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | 6 | namespace IdentityServerHost.Quickstart.UI 7 | { 8 | public class RedirectViewModel 9 | { 10 | public string RedirectUrl { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ConsentInputModel 10 | { 11 | public string Button { get; set; } 12 | public IEnumerable ScopesConsented { get; set; } 13 | public bool RememberConsent { get; set; } 14 | public string ReturnUrl { get; set; } 15 | public string Description { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ConsentViewModel : ConsentInputModel 10 | { 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public bool AllowRememberConsent { get; set; } 15 | 16 | public IEnumerable IdentityScopes { get; set; } 17 | public IEnumerable ApiScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Consent/ProcessConsentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ProcessConsentResult 10 | { 11 | public bool IsRedirect => RedirectUri != null; 12 | public string RedirectUri { get; set; } 13 | public Client Client { get; set; } 14 | 15 | public bool ShowView => ViewModel != null; 16 | public ConsentViewModel ViewModel { get; set; } 17 | 18 | public bool HasValidationError => ValidationError != null; 19 | public string ValidationError { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Value { get; set; } 10 | public string DisplayName { get; set; } 11 | public string Description { get; set; } 12 | public bool Emphasize { get; set; } 13 | public bool Required { get; set; } 14 | public bool Checked { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Device/DeviceAuthorizationInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class DeviceAuthorizationInputModel : ConsentInputModel 8 | { 9 | public string UserCode { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Device/DeviceAuthorizationViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class DeviceAuthorizationViewModel : ConsentViewModel 8 | { 9 | public string UserCode { get; set; } 10 | public bool ConfirmUserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Diagnostics/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace IdentityServerHost.Quickstart.UI 12 | { 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class DiagnosticsController : Controller 16 | { 17 | public async Task Index() 18 | { 19 | var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; 20 | if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) 21 | { 22 | return NotFound(); 23 | } 24 | 25 | var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); 26 | return View(model); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Diagnostics/DiagnosticsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Newtonsoft.Json; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | 11 | namespace IdentityServerHost.Quickstart.UI 12 | { 13 | public class DiagnosticsViewModel 14 | { 15 | public DiagnosticsViewModel(AuthenticateResult result) 16 | { 17 | AuthenticateResult = result; 18 | 19 | if (result.Properties.Items.ContainsKey("client_list")) 20 | { 21 | var encoded = result.Properties.Items["client_list"]; 22 | var bytes = Base64Url.Decode(encoded); 23 | var value = Encoding.UTF8.GetString(bytes); 24 | 25 | Clients = JsonConvert.DeserializeObject(value); 26 | } 27 | } 28 | 29 | public AuthenticateResult AuthenticateResult { get; } 30 | public IEnumerable Clients { get; } = new List(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using IdentityServer4.Models; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public static class Extensions 8 | { 9 | /// 10 | /// Checks if the redirect URI is for a native client. 11 | /// 12 | /// 13 | public static bool IsNativeClient(this AuthorizationRequest context) 14 | { 15 | return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) 16 | && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); 17 | } 18 | 19 | public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri) 20 | { 21 | controller.HttpContext.Response.StatusCode = 200; 22 | controller.HttpContext.Response.Headers["Location"] = ""; 23 | 24 | return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Grants/GrantsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServerHost.Quickstart.UI 9 | { 10 | public class GrantsViewModel 11 | { 12 | public IEnumerable Grants { get; set; } 13 | } 14 | 15 | public class GrantViewModel 16 | { 17 | public string ClientId { get; set; } 18 | public string ClientName { get; set; } 19 | public string ClientUrl { get; set; } 20 | public string ClientLogoUrl { get; set; } 21 | public string Description { get; set; } 22 | public DateTime Created { get; set; } 23 | public DateTime? Expires { get; set; } 24 | public IEnumerable IdentityGrantNames { get; set; } 25 | public IEnumerable ApiGrantNames { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorViewModel() 12 | { 13 | } 14 | 15 | public ErrorViewModel(string error) 16 | { 17 | Error = new ErrorMessage { Error = error }; 18 | } 19 | 20 | public ErrorMessage Error { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/IdentityService/Quickstart/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using System.Threading.Tasks; 12 | 13 | namespace IdentityServerHost.Quickstart.UI 14 | { 15 | [SecurityHeaders] 16 | [AllowAnonymous] 17 | public class HomeController : Controller 18 | { 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly IWebHostEnvironment _environment; 21 | private readonly ILogger _logger; 22 | 23 | public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment environment, ILogger logger) 24 | { 25 | _interaction = interaction; 26 | _environment = environment; 27 | _logger = logger; 28 | } 29 | 30 | public IActionResult Index() 31 | { 32 | if (_environment.IsDevelopment()) 33 | { 34 | // only show in development 35 | return View(); 36 | } 37 | 38 | _logger.LogInformation("Homepage is disabled in production. Returning 404."); 39 | return NotFound(); 40 | } 41 | 42 | /// 43 | /// Shows the error page 44 | /// 45 | public async Task Error(string errorId) 46 | { 47 | var vm = new ErrorViewModel(); 48 | 49 | // retrieve error details from identityserver 50 | var message = await _interaction.GetErrorContextAsync(errorId); 51 | if (message != null) 52 | { 53 | vm.Error = message; 54 | 55 | if (!_environment.IsDevelopment()) 56 | { 57 | // only show in development 58 | message.ErrorDescription = null; 59 | } 60 | } 61 | 62 | return View("Error", vm); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/IdentityService/Views/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Access Denied

5 |

You do not have access to that resource.

6 |
7 |
-------------------------------------------------------------------------------- /src/IdentityService/Views/Account/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @model LoggedOutViewModel 2 | 3 | @{ 4 | // set this so the layout rendering sees an anonymous user 5 | ViewData["signed-out"] = true; 6 | } 7 | 8 |
9 |

10 | Logout 11 | You are now logged out 12 |

13 | 14 | @if (Model.PostLogoutRedirectUri != null) 15 | { 16 |
17 | Click here to return to the 18 | @Model.ClientName application. 19 |
20 | } 21 | 22 | @if (Model.SignOutIframeUrl != null) 23 | { 24 | 25 | } 26 |
27 | 28 | @section scripts 29 | { 30 | @if (Model.AutomaticRedirectAfterSignOut) 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @model LogoutViewModel 2 | 3 |
4 |
5 |

Logout

6 |

Would you like to logut of IdentityServer?

7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Success

5 |

You have successfully authorized the device

6 |
7 |
8 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Device/UserCodeCapture.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 |
4 |
5 |

User Code

6 |

Please enter the code displayed on your device.

7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Diagnostics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DiagnosticsViewModel 2 | 3 |
4 |
5 |

Authentication Cookie

6 |
7 | 8 |
9 |
10 |
11 |
12 |

Claims

13 |
14 |
15 |
16 | @foreach (var claim in Model.AuthenticateResult.Principal.Claims) 17 | { 18 |
@claim.Type
19 |
@claim.Value
20 | } 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |

Properties

30 |
31 |
32 |
33 | @foreach (var prop in Model.AuthenticateResult.Properties.Items) 34 | { 35 |
@prop.Key
36 |
@prop.Value
37 | } 38 | @if (Model.Clients.Any()) 39 | { 40 |
Clients
41 |
42 | @{ 43 | var clients = Model.Clients.ToArray(); 44 | for(var i = 0; i < clients.Length; i++) 45 | { 46 | @clients[i] 47 | if (i < clients.Length - 1) 48 | { 49 | , 50 | } 51 | } 52 | } 53 |
54 | } 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Diagnostics 2 | 3 | @{ 4 | var version = FileVersionInfo.GetVersionInfo(typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); 5 | } 6 | 7 |
8 |

9 | 10 | Welcome to IdentityServer4 11 | (version @version) 12 |

13 | 14 |
    15 |
  • 16 | IdentityServer publishes a 17 | discovery document 18 | where you can find metadata and links to all the endpoints, key material, etc. 19 |
  • 20 |
  • 21 | Click here to see the claims for your current session. 22 |
  • 23 |
  • 24 | Click here to manage your stored grants. 25 |
  • 26 |
  • 27 | Here are links to the 28 | source code repository, 29 | and ready to use samples. 30 |
  • 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | 3 | @{ 4 | var error = Model?.Error?.Error; 5 | var errorDescription = Model?.Error?.ErrorDescription; 6 | var request_id = Model?.Error?.RequestId; 7 | } 8 | 9 |
10 |
11 |

Error

12 |
13 | 14 |
15 |
16 |
17 | Sorry, there was an error 18 | 19 | @if (error != null) 20 | { 21 | 22 | 23 | : @error 24 | 25 | 26 | 27 | if (errorDescription != null) 28 | { 29 |
@errorDescription
30 | } 31 | } 32 |
33 | 34 | @if (request_id != null) 35 | { 36 |
Request Id: @request_id
37 | } 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/Redirect.cshtml: -------------------------------------------------------------------------------- 1 | @model RedirectViewModel 2 | 3 |
4 |
5 |

You are now being returned to the application

6 |

Once complete, you may close this tab.

7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | IdentityServer4 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | @RenderBody() 21 |
22 | 23 | 24 | 25 | 26 | @RenderSection("scripts", required: false) 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/_Nav.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Extensions 2 | 3 | @{ 4 | string name = null; 5 | if (!true.Equals(ViewData["signed-out"])) 6 | { 7 | name = Context.User?.GetDisplayName(); 8 | } 9 | } 10 | 11 | 34 | -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @model ScopeViewModel 2 | 3 |
  • 4 | 24 | @if (Model.Required) 25 | { 26 | (required) 27 | } 28 | @if (Model.Description != null) 29 | { 30 | 33 | } 34 |
  • -------------------------------------------------------------------------------- /src/IdentityService/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /src/IdentityService/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServerHost.Quickstart.UI 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /src/IdentityService/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/IdentityService/tempkey.jwk: -------------------------------------------------------------------------------- 1 | {"alg":"RS256","d":"JKnU9fpENiweOHaQd-Ignx-sJwlqunlJxHVlUsd1md6r9eEglBBpR594Y_qODEq2FttXApU7BhwuNbl5Fym4hr27PV4EiahhvTdIxwCDtLmry1R5fWU_f_dwHCWFeHBFWmdSKNicf1mbfn4Yz0vVidG9C3ZGzgs_SjRxnmTTE4s9caaNCKO5HcCBhFsLYbYGjgf_cBfxBBD7mNq2uq9ufyVjOwcunPs40EN6hjG-wIdevabF9evvPTIB9paCtHCNCUd-ep3BrB5qa9oeTY8_c7Qhpu_Jhs9hVjczyNHKiPyEOGt7OopgaBV78IyFOg6uJlFyYwEWU7GNsFxteRQ7gQ","dp":"OWx7qccoyz8MyQabVigAcCAs4cWXqaCAnFQt2zfK2p9CewwUy0PbHIsLsikkSi4bcv0nqUA798c1Ae6SOVxdNg6fA6Ta0KcQX5ZmB_Hgqt5K3W5-yRIqRywedgulwEneHQ9JLjE0yVO7-8lZZvNlfK7daBSTCnAAWv729kNwwkk","dq":"t6c7NEgSSVjoUm15SXIZm6SYzcfq5oKmOcDmZj1dMSVOfAKsxzSpv26gnvhPKH2sN6T3F1ERcNMh0fpC0eM1O_r0IHWCZQ33n5RNsCKEKurWu-z1jEcoa3_4FBem_l5jUIIzeQn-dlDO1hucktGWCpB6nEcgTl4IieVktU0b3qc","e":"AQAB","kid":"A2E1D6248BEC8C6167A2BE0CFD7D605A","kty":"RSA","n":"zbhSDlx0FhmyP5QvTvYEPFMCQEwjJ11gCc0Dr59lBsG0s9Zo9_S4SSJeDyTdyG7eXl2WDDELHqrg1K73Py9V7Y0EzVqEuiVsIjbWlOThmvH75sGvCGOGxv2MAHlfV3JGXvZ0yCBhUHDOuyASh3dWvjsQxYInXxShyQY4hcCO6Ya5hlVpdMUxBWkOy-mYLCY4tN1G507ZMhEsq7QQaLdnTBn4IlZWxqa3VWbneRJM3bW2X-NR9XICOOfiVksxmYtGJNccw9BQKE6xqSZ45yvGhmybX-Qvuv1ujA7Eot64tZLIuE7Vd6YgSXOWQDtfayw-GmJR8lYAIr53nCO9S9RCRQ","p":"36NXLEgWL7RscARymA52DYSroRwyIRr9s0dpUudynnzxnQjSK1X1LLKKVSSraCcHvqe-3X70Il5FJN6Iyrvi1BAkTTfG4kXANSpBVCfOS83yLsEVWiJ-GoGywJCEl-DR62SPU5Wthk5VyuW6_-P0UVzzgm8KJab06O1ZOefjXY8","q":"6300-YKVt6PjYw5g6b5FYRrVeRQ-XExWHJhn2zHg_ReqDqQox8Eir46aaAkWIKGBGJLZVEW0d9noMbTximVNPDXqDIUbC3oM8LKybB50VuLVImIbn8vaU-r2GRzAGqIZpilSXUeL__nvam6n37wWFEIAJ2_ut6k0IqE-SWgcoOs","qi":"1lIsqTXQVKHvVuHT63hO-rNBoqB5hgyG2kV96PWd2dbsJ6gnqYFjTonLMKf9oaONI9P5gZykH1-QoQuhVHExBtyrkysBWrEpMAm7y7VZT4CLnmEsaWmzMzYqk0DVM5cuWLXrcIy9AuWihrZAsR7CiClRaACP4PbjIYpcrIaHd-s"} -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .body-container { 2 | margin-top: 60px; 3 | padding-bottom: 40px; } 4 | 5 | .welcome-page li { 6 | list-style: none; 7 | padding: 4px; } 8 | 9 | .logged-out-page iframe { 10 | display: none; 11 | width: 0; 12 | height: 0; } 13 | 14 | .grants-page .card { 15 | margin-top: 20px; 16 | border-bottom: 1px solid lightgray; } 17 | .grants-page .card .card-title { 18 | font-size: 120%; 19 | font-weight: bold; } 20 | .grants-page .card .card-title img { 21 | width: 100px; 22 | height: 100px; } 23 | .grants-page .card label { 24 | font-weight: bold; } 25 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | .body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/css/site.scss: -------------------------------------------------------------------------------- 1 | .body-container { 2 | margin-top: 60px; 3 | padding-bottom:40px; 4 | } 5 | 6 | .welcome-page { 7 | li { 8 | list-style: none; 9 | padding: 4px; 10 | } 11 | } 12 | 13 | .logged-out-page { 14 | iframe { 15 | display: none; 16 | width: 0; 17 | height: 0; 18 | } 19 | } 20 | 21 | .grants-page { 22 | .card { 23 | margin-top: 20px; 24 | border-bottom: 1px solid lightgray; 25 | 26 | .card-title { 27 | img { 28 | width: 100px; 29 | height: 100px; 30 | } 31 | 32 | font-size: 120%; 33 | font-weight: bold; 34 | } 35 | 36 | label { 37 | font-weight: bold; 38 | } 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/src/IdentityService/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/src/IdentityService/wwwroot/icon.jpg -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudnative-netcore/modernstore/9569cc579b0f908d53f2654218d71d96ac1f4cc5/src/IdentityService/wwwroot/icon.png -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/lib/bootstrap/scss/_alert.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .alert { 6 | position: relative; 7 | padding: $alert-padding-y $alert-padding-x; 8 | margin-bottom: $alert-margin-bottom; 9 | border: $alert-border-width solid transparent; 10 | @include border-radius($alert-border-radius); 11 | } 12 | 13 | // Headings for larger alerts 14 | .alert-heading { 15 | // Specified to prevent conflicts of changing $headings-color 16 | color: inherit; 17 | } 18 | 19 | // Provide class for links that match alerts 20 | .alert-link { 21 | font-weight: $alert-link-font-weight; 22 | } 23 | 24 | 25 | // Dismissible alerts 26 | // 27 | // Expand the right padding and account for the close button's positioning. 28 | 29 | .alert-dismissible { 30 | padding-right: $close-font-size + $alert-padding-x * 2; 31 | 32 | // Adjust close link position 33 | .close { 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | padding: $alert-padding-y $alert-padding-x; 38 | color: inherit; 39 | } 40 | } 41 | 42 | 43 | // Alternate styles 44 | // 45 | // Generate contextual modifier classes for colorizing the alert. 46 | 47 | @each $color, $value in $theme-colors { 48 | .alert-#{$color} { 49 | @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/lib/bootstrap/scss/_badge.scss: -------------------------------------------------------------------------------- 1 | // Base class 2 | // 3 | // Requires one of the contextual, color modifier classes for `color` and 4 | // `background-color`. 5 | 6 | .badge { 7 | display: inline-block; 8 | padding: $badge-padding-y $badge-padding-x; 9 | @include font-size($badge-font-size); 10 | font-weight: $badge-font-weight; 11 | line-height: 1; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | @include border-radius($badge-border-radius); 16 | @include transition($badge-transition); 17 | 18 | @at-root a#{&} { 19 | @include hover-focus() { 20 | text-decoration: none; 21 | } 22 | } 23 | 24 | // Empty badges collapse automatically 25 | &:empty { 26 | display: none; 27 | } 28 | } 29 | 30 | // Quick fix for badges in buttons 31 | .btn .badge { 32 | position: relative; 33 | top: -1px; 34 | } 35 | 36 | // Pill badges 37 | // 38 | // Make them extra rounded with a modifier to replace v3's badges. 39 | 40 | .badge-pill { 41 | padding-right: $badge-pill-padding-x; 42 | padding-left: $badge-pill-padding-x; 43 | @include border-radius($badge-pill-border-radius); 44 | } 45 | 46 | // Colors 47 | // 48 | // Contextual variations (linked badges get darker on :hover). 49 | 50 | @each $color, $value in $theme-colors { 51 | .badge-#{$color} { 52 | @include badge-variant($value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/IdentityService/wwwroot/lib/bootstrap/scss/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | display: flex; 3 | flex-wrap: wrap; 4 | padding: $breadcrumb-padding-y $breadcrumb-padding-x; 5 | margin-bottom: $breadcrumb-margin-bottom; 6 | @include font-size($breadcrumb-font-size); 7 | list-style: none; 8 | background-color: $breadcrumb-bg; 9 | @include border-radius($breadcrumb-border-radius); 10 | } 11 | 12 | .breadcrumb-item { 13 | // The separator between breadcrumbs (by default, a forward-slash: "/") 14 | + .breadcrumb-item { 15 | padding-left: $breadcrumb-item-padding; 16 | 17 | &::before { 18 | display: inline-block; // Suppress underlining of the separator in modern browsers 19 | padding-right: $breadcrumb-item-padding; 20 | color: $breadcrumb-divider-color; 21 | content: escape-svg($breadcrumb-divider); 22 | } 23 | } 24 | 25 | // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built 26 | // without `