This is a simple example of an Angular component.
4 |
5 | ;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ CounterComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(CounterComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should display a title', waitForAsync(() => {
23 | const titleText = fixture.nativeElement.querySelector('h1').textContent;
24 | expect(titleText).toEqual('Counter');
25 | }));
26 |
27 | it('should start with count 0, then increments by 1 when clicked', waitForAsync(() => {
28 | const countElement = fixture.nativeElement.querySelector('strong');
29 | expect(countElement.textContent).toEqual('0');
30 |
31 | const incrementButton = fixture.nativeElement.querySelector('button');
32 | incrementButton.click();
33 | fixture.detectChanges();
34 | expect(countElement.textContent).toEqual('1');
35 | }));
36 | });
37 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/counter/counter.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-counter-component',
5 | templateUrl: './counter.component.html'
6 | })
7 | export class CounterComponent {
8 | public currentCount = 0;
9 |
10 | public incrementCounter() {
11 | this.currentCount++;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/fetch-data/fetch-data.component.html:
--------------------------------------------------------------------------------
1 | Weather forecast
2 |
3 | This component demonstrates fetching data from the server.
4 |
5 | Loading...
6 |
7 |
8 |
9 |
10 | Date |
11 | Temp. (C) |
12 | Temp. (F) |
13 | Summary |
14 |
15 |
16 |
17 |
18 | {{ forecast.date }} |
19 | {{ forecast.temperatureC }} |
20 | {{ forecast.temperatureF }} |
21 | {{ forecast.summary }} |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/fetch-data/fetch-data.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { WeatherForecastClient, WeatherForecast } from '../web-api-client';
3 |
4 | @Component({
5 | selector: 'app-fetch-data',
6 | templateUrl: './fetch-data.component.html'
7 | })
8 | export class FetchDataComponent {
9 | public forecasts: WeatherForecast[];
10 |
11 | constructor(private client: WeatherForecastClient) {
12 | client.get().subscribe(result => {
13 | this.forecasts = result;
14 | }, error => console.error(error));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/home/home.component.html:
--------------------------------------------------------------------------------
1 | Hello, world!
2 | Welcome to your new single-page application, built with:
3 |
8 | To help you get started, we've also set up:
9 |
10 | - Client-side navigation. For example, click Counter then Back to return here.
11 | - Angular CLI integration. In development mode, there's no need to run
ng serve
. It runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.
12 | - Efficient production builds. In production mode, development-time features are disabled, and your
dotnet publish
configuration automatically invokes ng build
to produce minified, ahead-of-time compiled JavaScript files.
13 |
14 | The ClientApp
subdirectory is a standard Angular CLI application. If you open a command prompt in that directory, you can run any ng
command (e.g., ng test
), or use npm
to install extra packages into it.
15 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-home',
5 | templateUrl: './home.component.html',
6 | })
7 | export class HomeComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/nav-menu/dev-env.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree } from "@angular/router";
3 | import { Observable } from "rxjs";
4 | import { environment } from "src/environments/environment";
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class DevEnvGuard implements CanActivate {
10 | constructor() {}
11 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
12 | return !environment.production;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/nav-menu/nav-menu.component.html:
--------------------------------------------------------------------------------
1 |
61 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/nav-menu/nav-menu.component.scss:
--------------------------------------------------------------------------------
1 | a.navbar-brand {
2 | white-space: normal;
3 | text-align: center;
4 | word-break: break-all;
5 | }
6 |
7 | html {
8 | font-size: 14px;
9 | }
10 | @media (min-width: 768px) {
11 | html {
12 | font-size: 16px;
13 | }
14 | }
15 |
16 | .box-shadow {
17 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
18 | }
19 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/nav-menu/nav-menu.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { environment } from 'src/environments/environment';
3 |
4 | @Component({
5 | selector: 'app-nav-menu',
6 | templateUrl: './nav-menu.component.html',
7 | styleUrls: ['./nav-menu.component.scss']
8 | })
9 | export class NavMenuComponent implements OnInit {
10 | isExpanded = false;
11 |
12 | isProduction: boolean = false;
13 |
14 | ngOnInit() : void {
15 | this.isProduction = environment.production;
16 | }
17 |
18 | collapse() {
19 | this.isExpanded = false;
20 | }
21 |
22 | toggle() {
23 | this.isExpanded = !this.isExpanded;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/todo/todo.component.scss:
--------------------------------------------------------------------------------
1 | #listOptions {
2 | margin-right: 10px;
3 | }
4 |
5 | #todo-items {
6 | .item-input-control {
7 | border: 0;
8 | box-shadow: none;
9 | background-color: transparent;
10 | }
11 |
12 | .done-todo {
13 | text-decoration: line-through;
14 | }
15 |
16 | .todo-item-title {
17 | padding-top: 8px;
18 | }
19 |
20 | .list-group-item {
21 | padding-top: 8px;
22 | padding-bottom: 8px;
23 |
24 | .btn-xs {
25 | padding: 0;
26 | }
27 | }
28 |
29 | .todo-item-checkbox {
30 | padding-top: 8px;
31 | }
32 |
33 | .todo-item-commands {
34 | padding-top: 4px;
35 | }
36 | }
37 |
38 | .modal-footer {
39 | display: block;
40 | }
41 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/token/token.component.html:
--------------------------------------------------------------------------------
1 | JWT
2 |
3 |
4 |
5 | This component demonstrates interacting with the authorization service to
6 | retrieve your
7 | JSON web token (JWT).
8 |
9 |
14 |
15 |
16 |
19 |
20 |
21 | Copied!
22 |
23 |
24 |
25 |
26 |
Error getting JWT
27 |
28 | Something went wrong getting your access token from the auth service. Please
29 | try again.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/app/token/token.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from "@angular/core";
2 | import { AuthorizeService } from "../../api-authorization/authorize.service";
3 |
4 | import { faCopy } from "@fortawesome/free-solid-svg-icons";
5 |
6 | @Component({
7 | selector: "app-token-component",
8 | templateUrl: "./token.component.html",
9 | })
10 | export class TokenComponent implements OnInit {
11 | token: string;
12 | isError: boolean;
13 | isCopied: boolean;
14 |
15 | faCopy = faCopy;
16 |
17 | constructor(private authorizeService: AuthorizeService) {}
18 |
19 | ngOnInit(): void {
20 | this.isCopied = false;
21 | this.authorizeService.getAccessToken().subscribe(
22 | (t) => {
23 | this.token = "Bearer " + t;
24 | this.isError = false;
25 | },
26 | (err) => {
27 | this.isError = true;
28 | }
29 | );
30 | }
31 |
32 | copyToClipboard(): void {
33 | const selBox = document.createElement("textarea");
34 | selBox.style.position = "fixed";
35 | selBox.style.left = "0";
36 | selBox.style.top = "0";
37 | selBox.style.opacity = "0";
38 | selBox.value = this.token;
39 | document.body.appendChild(selBox);
40 | selBox.focus();
41 | selBox.select();
42 | document.execCommand("copy");
43 | document.body.removeChild(selBox);
44 | this.isCopied = true;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirzaf/CleanArchitecture/35b490110f699c2ba427fd6b49e98babc16390c0/src/WebUI/ClientApp/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * In development mode, to ignore zone related error stack frames such as
11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can
12 | * import the following file, but please comment it out in production mode
13 | * because it will have performance impact when throw error
14 | */
15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
16 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CleanArchitecture
6 |
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, '../coverage'),
20 | reports: ['html', 'lcovonly'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | export function getBaseUrl() {
8 | return document.getElementsByTagName('base')[0].href;
9 | }
10 |
11 | const providers = [
12 | { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }
13 | ];
14 |
15 | if (environment.production) {
16 | enableProdMode();
17 | }
18 |
19 | platformBrowserDynamic(providers).bootstrapModule(AppModule)
20 | .catch(err => console.log(err));
21 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags.ts';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | /* Provide sufficient contrast against white background */
4 | a {
5 | color: #0366d6;
6 | }
7 |
8 | code {
9 | color: #e01a76;
10 | }
11 |
12 | .btn-primary {
13 | color: #fff;
14 | background-color: #1b6ec2;
15 | border-color: #1861ac;
16 | }
17 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "main.ts",
9 | "polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2016"
5 | },
6 | "angularCompilerOptions": {
7 | "entryModule": "app/app.server.module#AppServerModule"
8 | }
,
9 | "files": [
10 | "main.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "test.ts",
12 | "polyfills.ts"
13 | ],
14 | "include": [
15 | "**/*.spec.ts",
16 | "**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "app",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "app",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "module": "es2020",
6 | "outDir": "./dist/out-tsc",
7 | "sourceMap": true,
8 | "declaration": false,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "target": "es2015",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "dom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/WebUI/ClientApp/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-var-keyword": true,
76 | "object-literal-sort-keys": false,
77 | "one-line": [
78 | true,
79 | "check-open-brace",
80 | "check-catch",
81 | "check-else",
82 | "check-whitespace"
83 | ],
84 | "prefer-const": true,
85 | "quotemark": [
86 | true,
87 | "single"
88 | ],
89 | "radix": true,
90 | "semicolon": [
91 | true,
92 | "always"
93 | ],
94 | "triple-equals": [
95 | true,
96 | "allow-null-check"
97 | ],
98 | "typedef-whitespace": [
99 | true,
100 | {
101 | "call-signature": "nospace",
102 | "index-signature": "nospace",
103 | "parameter": "nospace",
104 | "property-declaration": "nospace",
105 | "variable-declaration": "nospace"
106 | }
107 | ],
108 | "unified-signatures": true,
109 | "variable-name": false,
110 | "whitespace": [
111 | true,
112 | "check-branch",
113 | "check-decl",
114 | "check-operator",
115 | "check-separator",
116 | "check-type"
117 | ],
118 | "no-output-on-prefix": true,
119 | "no-inputs-metadata-property": true,
120 | "no-outputs-metadata-property": true,
121 | "no-host-metadata-property": true,
122 | "no-input-rename": true,
123 | "no-output-rename": true,
124 | "use-lifecycle-interface": true,
125 | "use-pipe-transform-interface": true,
126 | "component-class-suffix": true,
127 | "directive-class-suffix": true
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/WebUI/Controllers/ApiControllerBase.cs:
--------------------------------------------------------------------------------
1 | using MediatR;
2 |
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | namespace CleanArchitecture.WebUI.Controllers;
6 |
7 | [ApiController]
8 | [Route("api/[controller]")]
9 | public abstract class ApiControllerBase : ControllerBase
10 | {
11 | private ISender _mediator = null!;
12 |
13 | protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService();
14 | }
15 |
--------------------------------------------------------------------------------
/src/WebUI/Controllers/OidcConfigurationController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace CleanArchitecture.WebUI.Controllers;
5 |
6 | [ApiExplorerSettings(IgnoreApi = true)]
7 | public class OidcConfigurationController : Controller
8 | {
9 | private readonly ILogger logger;
10 |
11 | public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider, ILogger _logger)
12 | {
13 | ClientRequestParametersProvider = clientRequestParametersProvider;
14 | logger = _logger;
15 | }
16 |
17 | public IClientRequestParametersProvider ClientRequestParametersProvider { get; }
18 |
19 | [HttpGet("_configuration/{clientId}")]
20 | public IActionResult GetClientRequestParameters([FromRoute] string clientId)
21 | {
22 | var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId);
23 | return Ok(parameters);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/WebUI/Controllers/TodoItemsController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Models;
2 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
3 | using CleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem;
4 | using CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;
5 | using CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail;
6 | using CleanArchitecture.Application.TodoItems.Queries.GetTodoItemsWithPagination;
7 | using Microsoft.AspNetCore.Authorization;
8 | using Microsoft.AspNetCore.Mvc;
9 |
10 | namespace CleanArchitecture.WebUI.Controllers;
11 |
12 | [Authorize]
13 | public class TodoItemsController : ApiControllerBase
14 | {
15 | [HttpGet]
16 | public async Task>> GetTodoItemsWithPagination([FromQuery] GetTodoItemsWithPaginationQuery query)
17 | {
18 | return await Mediator.Send(query);
19 | }
20 |
21 | [HttpPost]
22 | public async Task> Create(CreateTodoItemCommand command)
23 | {
24 | return await Mediator.Send(command);
25 | }
26 |
27 | [HttpPut("{id}")]
28 | public async Task Update(int id, UpdateTodoItemCommand command)
29 | {
30 | if (id != command.Id)
31 | {
32 | return BadRequest();
33 | }
34 |
35 | await Mediator.Send(command);
36 |
37 | return NoContent();
38 | }
39 |
40 | [HttpPut("[action]")]
41 | public async Task UpdateItemDetails(int id, UpdateTodoItemDetailCommand command)
42 | {
43 | if (id != command.Id)
44 | {
45 | return BadRequest();
46 | }
47 |
48 | await Mediator.Send(command);
49 |
50 | return NoContent();
51 | }
52 |
53 | [HttpDelete("{id}")]
54 | public async Task Delete(int id)
55 | {
56 | await Mediator.Send(new DeleteTodoItemCommand { Id = id });
57 |
58 | return NoContent();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/WebUI/Controllers/TodoListsController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
2 | using CleanArchitecture.Application.TodoLists.Commands.DeleteTodoList;
3 | using CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;
4 | using CleanArchitecture.Application.TodoLists.Queries.ExportTodos;
5 | using CleanArchitecture.Application.TodoLists.Queries.GetTodos;
6 | using Microsoft.AspNetCore.Authorization;
7 | using Microsoft.AspNetCore.Mvc;
8 |
9 | namespace CleanArchitecture.WebUI.Controllers;
10 |
11 | [Authorize]
12 | public class TodoListsController : ApiControllerBase
13 | {
14 | [HttpGet]
15 | public async Task> Get()
16 | {
17 | return await Mediator.Send(new GetTodosQuery());
18 | }
19 |
20 | [HttpGet("{id}")]
21 | public async Task Get(int id)
22 | {
23 | var vm = await Mediator.Send(new ExportTodosQuery { ListId = id });
24 |
25 | return File(vm.Content, vm.ContentType, vm.FileName);
26 | }
27 |
28 | [HttpPost]
29 | public async Task> Create(CreateTodoListCommand command)
30 | {
31 | return await Mediator.Send(command);
32 | }
33 |
34 | [HttpPut("{id}")]
35 | public async Task Update(int id, UpdateTodoListCommand command)
36 | {
37 | if (id != command.Id)
38 | {
39 | return BadRequest();
40 | }
41 |
42 | await Mediator.Send(command);
43 |
44 | return NoContent();
45 | }
46 |
47 | [HttpDelete("{id}")]
48 | public async Task Delete(int id)
49 | {
50 | await Mediator.Send(new DeleteTodoListCommand { Id = id });
51 |
52 | return NoContent();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/WebUI/Controllers/WeatherForecastController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.WeatherForecasts.Queries.GetWeatherForecasts;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace CleanArchitecture.WebUI.Controllers;
5 |
6 | public class WeatherForecastController : ApiControllerBase
7 | {
8 | [HttpGet]
9 | public async Task> Get()
10 | {
11 | return await Mediator.Send(new GetWeatherForecastsQuery());
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/WebUI/Dockerfile:
--------------------------------------------------------------------------------
1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | # This stage is used for VS debugging on Docker
4 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
5 | ENV ASPNETCORE_URLS=https://+:5001;http://+:5000
6 | WORKDIR /app
7 | EXPOSE 5000
8 | EXPOSE 5001
9 |
10 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
11 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
12 | RUN apt install -y nodejs
13 | WORKDIR /src
14 | COPY ["src/WebUI/WebUI.csproj", "src/WebUI/"]
15 | COPY ["src/Application/Application.csproj", "src/Application/"]
16 | COPY ["src/Domain/Domain.csproj", "src/Domain/"]
17 | COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
18 | RUN dotnet restore "src/WebUI/WebUI.csproj"
19 | COPY . .
20 | WORKDIR "/src/src/WebUI"
21 | RUN dotnet build "WebUI.csproj" -c Release -o /app/build
22 |
23 | FROM build AS publish
24 | RUN dotnet publish "WebUI.csproj" -c Release -o /app/publish
25 |
26 | FROM base AS final
27 | WORKDIR /app
28 | COPY --from=publish /app/publish .
29 | ENTRYPOINT ["dotnet", "CleanArchitecture.WebUI.dll"]
--------------------------------------------------------------------------------
/src/WebUI/Filters/ApiExceptionFilterAttribute.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 |
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Mvc.Filters;
5 |
6 | namespace CleanArchitecture.WebUI.Filters;
7 |
8 | public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
9 | {
10 |
11 | private readonly IDictionary> _exceptionHandlers;
12 |
13 | public ApiExceptionFilterAttribute()
14 | {
15 | // Register known exception types and handlers.
16 | _exceptionHandlers = new Dictionary>
17 | {
18 | { typeof(ValidationException), HandleValidationException },
19 | { typeof(NotFoundException), HandleNotFoundException },
20 | { typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
21 | { typeof(ForbiddenAccessException), HandleForbiddenAccessException },
22 | };
23 | }
24 |
25 | public override void OnException(ExceptionContext context)
26 | {
27 | HandleException(context);
28 |
29 | base.OnException(context);
30 | }
31 |
32 | private void HandleException(ExceptionContext context)
33 | {
34 | Type type = context.Exception.GetType();
35 | if (_exceptionHandlers.ContainsKey(type))
36 | {
37 | _exceptionHandlers[type].Invoke(context);
38 | return;
39 | }
40 |
41 | if (!context.ModelState.IsValid)
42 | {
43 | HandleInvalidModelStateException(context);
44 | return;
45 | }
46 |
47 | HandleUnknownException(context);
48 | }
49 |
50 | private void HandleValidationException(ExceptionContext context)
51 | {
52 | var exception = (ValidationException)context.Exception;
53 |
54 | var details = new ValidationProblemDetails(exception.Errors)
55 | {
56 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
57 | };
58 |
59 | context.Result = new BadRequestObjectResult(details);
60 |
61 | context.ExceptionHandled = true;
62 | }
63 |
64 | private void HandleInvalidModelStateException(ExceptionContext context)
65 | {
66 | var details = new ValidationProblemDetails(context.ModelState)
67 | {
68 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
69 | };
70 |
71 | context.Result = new BadRequestObjectResult(details);
72 |
73 | context.ExceptionHandled = true;
74 | }
75 |
76 | private void HandleNotFoundException(ExceptionContext context)
77 | {
78 | var exception = (NotFoundException)context.Exception;
79 |
80 | var details = new ProblemDetails()
81 | {
82 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
83 | Title = "The specified resource was not found.",
84 | Detail = exception.Message
85 | };
86 |
87 | context.Result = new NotFoundObjectResult(details);
88 |
89 | context.ExceptionHandled = true;
90 | }
91 |
92 | private void HandleUnauthorizedAccessException(ExceptionContext context)
93 | {
94 | var details = new ProblemDetails
95 | {
96 | Status = StatusCodes.Status401Unauthorized,
97 | Title = "Unauthorized",
98 | Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
99 | };
100 |
101 | context.Result = new ObjectResult(details)
102 | {
103 | StatusCode = StatusCodes.Status401Unauthorized
104 | };
105 |
106 | context.ExceptionHandled = true;
107 | }
108 |
109 | private void HandleForbiddenAccessException(ExceptionContext context)
110 | {
111 | var details = new ProblemDetails
112 | {
113 | Status = StatusCodes.Status403Forbidden,
114 | Title = "Forbidden",
115 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
116 | };
117 |
118 | context.Result = new ObjectResult(details)
119 | {
120 | StatusCode = StatusCodes.Status403Forbidden
121 | };
122 |
123 | context.ExceptionHandled = true;
124 | }
125 |
126 | private void HandleUnknownException(ExceptionContext context)
127 | {
128 | var details = new ProblemDetails
129 | {
130 | Status = StatusCodes.Status500InternalServerError,
131 | Title = "An error occurred while processing your request.",
132 | Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
133 | };
134 |
135 | context.Result = new ObjectResult(details)
136 | {
137 | StatusCode = StatusCodes.Status500InternalServerError
138 | };
139 |
140 | context.ExceptionHandled = true;
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/WebUI/Pages/Error.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model ErrorModel
3 | @{
4 | ViewData["Title"] = "Error";
5 | }
6 |
7 | Error.
8 | An error occurred while processing your request.
9 |
10 | @if (Model.ShowRequestId)
11 | {
12 |
13 | Request ID: @Model.RequestId
14 |
15 | }
16 |
17 | Development Mode
18 |
19 | Swapping to the Development environment displays detailed information about the error that occurred.
20 |
21 |
22 | The Development environment shouldn't be enabled for deployed applications.
23 | It can result in displaying sensitive information from exceptions to end users.
24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
25 | and restarting the app.
26 |
27 |
--------------------------------------------------------------------------------
/src/WebUI/Pages/Error.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Mvc.RazorPages;
5 |
6 | namespace CleanArchitecture.WebUI.Pages;
7 |
8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
9 | public class ErrorModel : PageModel
10 | {
11 | private readonly ILogger _logger;
12 |
13 | public ErrorModel(ILogger logger)
14 | {
15 | _logger = logger;
16 | }
17 |
18 | public string? RequestId { get; set; }
19 |
20 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
21 |
22 | public void OnGet()
23 | {
24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/WebUI/Pages/Shared/_LoginPartial.cshtml:
--------------------------------------------------------------------------------
1 | @using Microsoft.AspNetCore.Identity
2 | @using CleanArchitecture.Infrastructure.Identity;
3 | @inject SignInManager SignInManager
4 | @inject UserManager UserManager
5 |
6 | @{
7 | string returnUrl = null;
8 | var query = ViewContext.HttpContext.Request.Query;
9 | if (query.ContainsKey("returnUrl"))
10 | {
11 | returnUrl = query["returnUrl"];
12 | }
13 | }
14 |
15 |
37 |
--------------------------------------------------------------------------------
/src/WebUI/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using CleanArchitecture.WebUI
2 | @namespace CleanArchitecture.WebUI.Pages
3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4 |
--------------------------------------------------------------------------------
/src/WebUI/Program.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Infrastructure.Identity;
2 | using CleanArchitecture.Infrastructure.Persistence;
3 | using Microsoft.AspNetCore.Identity;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace CleanArchitecture.WebUI;
7 |
8 | public class Program
9 | {
10 | public async static Task Main(string[] args)
11 | {
12 | var host = CreateHostBuilder(args).Build();
13 |
14 | using (var scope = host.Services.CreateScope())
15 | {
16 | var services = scope.ServiceProvider;
17 |
18 | try
19 | {
20 | var context = services.GetRequiredService();
21 |
22 | if (context.Database.IsSqlServer())
23 | {
24 | context.Database.Migrate();
25 | }
26 |
27 | var userManager = services.GetRequiredService>();
28 | var roleManager = services.GetRequiredService>();
29 |
30 | await ApplicationDbContextSeed.SeedDefaultUserAsync(userManager, roleManager);
31 | await ApplicationDbContextSeed.SeedSampleDataAsync(context);
32 | }
33 | catch (Exception ex)
34 | {
35 | var logger = scope.ServiceProvider.GetRequiredService>();
36 |
37 | logger.LogError(ex, "An error occurred while migrating or seeding the database.");
38 |
39 | throw;
40 | }
41 | }
42 |
43 | await host.RunAsync();
44 | }
45 |
46 | public static IHostBuilder CreateHostBuilder(string[] args) =>
47 | Host.CreateDefaultBuilder(args)
48 | .ConfigureWebHostDefaults(webBuilder =>
49 | webBuilder.UseStartup());
50 | }
51 |
--------------------------------------------------------------------------------
/src/WebUI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:61846",
7 | "sslPort": 44312
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "DOTNET_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "CleanArchitecture.WebUI": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "environmentVariables": {
22 | "DOTNET_ENVIRONMENT": "Development"
23 | },
24 | "applicationUrl": "https://localhost:5001;http://localhost:5000"
25 | },
26 | "Docker": {
27 | "commandName": "Docker",
28 | "launchBrowser": true,
29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
30 | "environmentVariables": {
31 | "UseInMemoryDatabase": "true",
32 | "ASPNETCORE_HTTPS_PORT": "5001",
33 | "ASPNETCORE_URLS": "https://+:5001;http://+:5000"
34 | },
35 | "httpPort": 5000,
36 | "useSSL": true,
37 | "sslPort": 5001
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/WebUI/Services/CurrentUserService.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 |
3 | using CleanArchitecture.Application.Common.Interfaces;
4 |
5 | namespace CleanArchitecture.WebUI.Services;
6 |
7 | public class CurrentUserService : ICurrentUserService
8 | {
9 | private readonly IHttpContextAccessor _httpContextAccessor;
10 |
11 | public CurrentUserService(IHttpContextAccessor httpContextAccessor)
12 | {
13 | _httpContextAccessor = httpContextAccessor;
14 | }
15 |
16 | public string? UserId => _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
17 | }
18 |
--------------------------------------------------------------------------------
/src/WebUI/Startup.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application;
2 | using CleanArchitecture.Application.Common.Interfaces;
3 | using CleanArchitecture.Infrastructure;
4 | using CleanArchitecture.Infrastructure.Persistence;
5 | using CleanArchitecture.WebUI.Filters;
6 | using CleanArchitecture.WebUI.Services;
7 | using FluentValidation.AspNetCore;
8 | using Microsoft.AspNetCore.Mvc;
9 | using NSwag;
10 | using NSwag.Generation.Processors.Security;
11 |
12 | namespace CleanArchitecture.WebUI;
13 |
14 | public class Startup
15 | {
16 | public Startup(IConfiguration configuration)
17 | {
18 | Configuration = configuration;
19 | }
20 |
21 | public IConfiguration Configuration { get; }
22 |
23 | // This method gets called by the runtime. Use this method to add services to the container.
24 | public void ConfigureServices(IServiceCollection services)
25 | {
26 | services.AddApplication();
27 | services.AddInfrastructure(Configuration);
28 |
29 | services.AddDatabaseDeveloperPageExceptionFilter();
30 |
31 | services.AddSingleton();
32 |
33 | services.AddHttpContextAccessor();
34 |
35 | services.AddHealthChecks()
36 | .AddDbContextCheck();
37 |
38 | services.AddControllersWithViews(options =>
39 | options.Filters.Add())
40 | .AddFluentValidation(x => x.AutomaticValidationEnabled = false);
41 |
42 | services.AddRazorPages();
43 |
44 | // Customise default API behaviour
45 | services.Configure(options =>
46 | options.SuppressModelStateInvalidFilter = true);
47 |
48 | // In production, the Angular files will be served from this directory
49 | services.AddSpaStaticFiles(configuration =>
50 | configuration.RootPath = "ClientApp/dist");
51 |
52 | services.AddOpenApiDocument(configure =>
53 | {
54 | configure.Title = "CleanArchitecture API";
55 | configure.AddSecurity("JWT", Enumerable.Empty(), new OpenApiSecurityScheme
56 | {
57 | Type = OpenApiSecuritySchemeType.ApiKey,
58 | Name = "Authorization",
59 | In = OpenApiSecurityApiKeyLocation.Header,
60 | Description = "Type into the textbox: Bearer {your JWT token}."
61 | });
62 |
63 | configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
64 | });
65 | }
66 |
67 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
68 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
69 | {
70 | if (env.IsDevelopment())
71 | {
72 | app.UseDeveloperExceptionPage();
73 | app.UseMigrationsEndPoint();
74 | }
75 | else
76 | {
77 | app.UseExceptionHandler("/Error");
78 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
79 | app.UseHsts();
80 | }
81 |
82 | app.UseHealthChecks("/health");
83 | app.UseHttpsRedirection();
84 | app.UseStaticFiles();
85 | if (!env.IsDevelopment())
86 | {
87 | app.UseSpaStaticFiles();
88 | }
89 |
90 | app.UseSwaggerUi3(settings =>
91 | {
92 | settings.Path = "/api";
93 | settings.DocumentPath = "/api/specification.json";
94 | });
95 |
96 | app.UseRouting();
97 |
98 | app.UseAuthentication();
99 | app.UseIdentityServer();
100 | app.UseAuthorization();
101 | app.UseEndpoints(endpoints =>
102 | {
103 | endpoints.MapControllerRoute(
104 | name: "default",
105 | pattern: "{controller}/{action=Index}/{id?}");
106 | endpoints.MapRazorPages();
107 | });
108 |
109 | app.UseSpa(spa =>
110 | {
111 | // To learn more about options for serving an Angular SPA from ASP.NET Core,
112 | // see https://go.microsoft.com/fwlink/?linkid=864501
113 |
114 | spa.Options.SourcePath = "ClientApp";
115 |
116 | if (env.IsDevelopment())
117 | {
118 | //spa.UseAngularCliServer(npmScript: "start");
119 | spa.UseProxyToSpaDevelopmentServer(Configuration["SpaBaseUrl"] ?? "http://localhost:4200");
120 | }
121 | });
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/WebUI/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | },
9 | "IdentityServer": {
10 | "Key": {
11 | "Type": "Development"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/WebUI/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "UseInMemoryDatabase": false,
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | },
10 | "IdentityServer": {
11 | "Key": {
12 | "Type": "Store",
13 | "StoreName": "My",
14 | "StoreLocation": "CurrentUser",
15 | "Name": "CN=MyApplication"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/WebUI/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "UseInMemoryDatabase": true,
3 | "ConnectionStrings": {
4 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CleanArchitectureDb;Trusted_Connection=True;MultipleActiveResultSets=true;"
5 | },
6 | "Logging": {
7 | "LogLevel": {
8 | "Default": "Warning"
9 | }
10 | },
11 | "IdentityServer": {
12 | "Clients": {
13 | "CleanArchitecture.WebUI": {
14 | "Profile": "IdentityServerSPA"
15 | }
16 | }
17 | },
18 | "AllowedHosts": "*"
19 | }
20 |
--------------------------------------------------------------------------------
/src/WebUI/nswag.json:
--------------------------------------------------------------------------------
1 | {
2 | "runtime": "Net60",
3 | "defaultVariables": null,
4 | "documentGenerator": {
5 | "aspNetCoreToOpenApi": {
6 | "project": "WebUI.csproj",
7 | "msBuildProjectExtensionsPath": null,
8 | "configuration": null,
9 | "runtime": null,
10 | "targetFramework": null,
11 | "noBuild": true,
12 | "verbose": false,
13 | "workingDirectory": null,
14 | "requireParametersWithoutDefault": true,
15 | "apiGroupNames": null,
16 | "defaultPropertyNameHandling": "CamelCase",
17 | "defaultReferenceTypeNullHandling": "Null",
18 | "defaultDictionaryValueReferenceTypeNullHandling": "NotNull",
19 | "defaultResponseReferenceTypeNullHandling": "NotNull",
20 | "defaultEnumHandling": "Integer",
21 | "flattenInheritanceHierarchy": false,
22 | "generateKnownTypes": true,
23 | "generateEnumMappingDescription": false,
24 | "generateXmlObjects": false,
25 | "generateAbstractProperties": false,
26 | "generateAbstractSchemas": true,
27 | "ignoreObsoleteProperties": false,
28 | "allowReferencesWithProperties": false,
29 | "excludedTypeNames": [],
30 | "serviceHost": null,
31 | "serviceBasePath": null,
32 | "serviceSchemes": [],
33 | "infoTitle": "CleanArchitecture API",
34 | "infoDescription": null,
35 | "infoVersion": "1.0.0",
36 | "documentTemplate": null,
37 | "documentProcessorTypes": [],
38 | "operationProcessorTypes": [],
39 | "typeNameGeneratorType": null,
40 | "schemaNameGeneratorType": null,
41 | "contractResolverType": null,
42 | "serializerSettingsType": null,
43 | "useDocumentProvider": true,
44 | "documentName": "v1",
45 | "aspNetCoreEnvironment": null,
46 | "createWebHostBuilderMethod": null,
47 | "startupType": null,
48 | "allowNullableBodyParameters": true,
49 | "output": "wwwroot/api/specification.json",
50 | "outputType": "OpenApi3",
51 | "assemblyPaths": [],
52 | "assemblyConfig": null,
53 | "referencePaths": [],
54 | "useNuGetCache": false
55 | }
56 | },
57 | "codeGenerators": {
58 | "openApiToTypeScriptClient": {
59 | "className": "{controller}Client",
60 | "moduleName": "",
61 | "namespace": "",
62 | "typeScriptVersion": 2.7,
63 | "template": "Angular",
64 | "promiseType": "Promise",
65 | "httpClass": "HttpClient",
66 | "withCredentials": false,
67 | "useSingletonProvider": true,
68 | "injectionTokenType": "InjectionToken",
69 | "rxJsVersion": 6.0,
70 | "dateTimeType": "Date",
71 | "nullValue": "Undefined",
72 | "generateClientClasses": true,
73 | "generateClientInterfaces": true,
74 | "generateOptionalParameters": false,
75 | "exportTypes": true,
76 | "wrapDtoExceptions": false,
77 | "exceptionClass": "SwaggerException",
78 | "clientBaseClass": null,
79 | "wrapResponses": false,
80 | "wrapResponseMethods": [],
81 | "generateResponseClasses": true,
82 | "responseClass": "SwaggerResponse",
83 | "protectedMethods": [],
84 | "configurationClass": null,
85 | "useTransformOptionsMethod": false,
86 | "useTransformResultMethod": false,
87 | "generateDtoTypes": true,
88 | "operationGenerationMode": "MultipleClientsFromOperationId",
89 | "markOptionalProperties": true,
90 | "generateCloneMethod": false,
91 | "typeStyle": "Class",
92 | "classTypes": [],
93 | "extendedClasses": [],
94 | "extensionCode": null,
95 | "generateDefaultValues": true,
96 | "excludedTypeNames": [],
97 | "excludedParameterNames": [],
98 | "handleReferences": false,
99 | "generateConstructorInterface": true,
100 | "convertConstructorInterfaceData": false,
101 | "importRequiredTypes": true,
102 | "useGetBaseUrlMethod": false,
103 | "baseUrlTokenName": "API_BASE_URL",
104 | "queryNullValue": "",
105 | "inlineNamedDictionaries": false,
106 | "inlineNamedAny": false,
107 | "templateDirectory": null,
108 | "typeNameGeneratorType": null,
109 | "propertyNameGeneratorType": null,
110 | "enumNameGeneratorType": null,
111 | "serviceHost": null,
112 | "serviceSchemes": null,
113 | "output": "ClientApp/src/app/web-api-client.ts"
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/WebUI/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirzaf/CleanArchitecture/35b490110f699c2ba427fd6b49e98babc16390c0/src/WebUI/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/Application.IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | CleanArchitecture.Application.IntegrationTests
6 | CleanArchitecture.Application.IntegrationTests
7 |
8 | false
9 | enable
10 | enable
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Always
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | all
28 | runtime; build; native; contentfiles; analyzers; buildtransitive
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TestBase.cs:
--------------------------------------------------------------------------------
1 | using NUnit.Framework;
2 |
3 | namespace CleanArchitecture.Application.IntegrationTests;
4 |
5 | using static Testing;
6 |
7 | public class TestBase
8 | {
9 | [SetUp]
10 | public async Task TestSetUp()
11 | {
12 | await ResetState();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoItems/Commands/CreateTodoItemTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
3 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
4 | using CleanArchitecture.Domain.Entities;
5 | using FluentAssertions;
6 | using NUnit.Framework;
7 |
8 | namespace CleanArchitecture.Application.IntegrationTests.TodoItems.Commands;
9 |
10 | using static Testing;
11 |
12 | public class CreateTodoItemTests : TestBase
13 | {
14 | [Test]
15 | public async Task ShouldRequireMinimumFields()
16 | {
17 | var command = new CreateTodoItemCommand();
18 |
19 | await FluentActions.Invoking(() =>
20 | SendAsync(command)).Should().ThrowAsync();
21 | }
22 |
23 | [Test]
24 | public async Task ShouldCreateTodoItem()
25 | {
26 | var userId = await RunAsDefaultUserAsync();
27 |
28 | var listId = await SendAsync(new CreateTodoListCommand
29 | {
30 | Title = "New List"
31 | });
32 |
33 | var command = new CreateTodoItemCommand
34 | {
35 | ListId = listId,
36 | Title = "Tasks"
37 | };
38 |
39 | var itemId = await SendAsync(command);
40 |
41 | var item = await FindAsync(itemId);
42 |
43 | item.Should().NotBeNull();
44 | item!.ListId.Should().Be(command.ListId);
45 | item.Title.Should().Be(command.Title);
46 | item.CreatedBy.Should().Be(userId);
47 | item.Created.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
48 | item.LastModifiedBy.Should().BeNull();
49 | item.LastModified.Should().BeNull();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoItems/Commands/DeleteTodoItemTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
3 | using CleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem;
4 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
5 | using CleanArchitecture.Domain.Entities;
6 | using FluentAssertions;
7 | using NUnit.Framework;
8 |
9 | namespace CleanArchitecture.Application.IntegrationTests.TodoItems.Commands;
10 |
11 | using static Testing;
12 |
13 | public class DeleteTodoItemTests : TestBase
14 | {
15 | [Test]
16 | public async Task ShouldRequireValidTodoItemId()
17 | {
18 | var command = new DeleteTodoItemCommand { Id = 99 };
19 |
20 | await FluentActions.Invoking(() =>
21 | SendAsync(command)).Should().ThrowAsync();
22 | }
23 |
24 | [Test]
25 | public async Task ShouldDeleteTodoItem()
26 | {
27 | var listId = await SendAsync(new CreateTodoListCommand
28 | {
29 | Title = "New List"
30 | });
31 |
32 | var itemId = await SendAsync(new CreateTodoItemCommand
33 | {
34 | ListId = listId,
35 | Title = "New Item"
36 | });
37 |
38 | await SendAsync(new DeleteTodoItemCommand
39 | {
40 | Id = itemId
41 | });
42 |
43 | var item = await FindAsync(itemId);
44 |
45 | item.Should().BeNull();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoItems/Commands/UpdateTodoItemDetailTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
3 | using CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;
4 | using CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail;
5 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
6 | using CleanArchitecture.Domain.Entities;
7 | using CleanArchitecture.Domain.Enums;
8 | using FluentAssertions;
9 | using NUnit.Framework;
10 |
11 | namespace CleanArchitecture.Application.IntegrationTests.TodoItems.Commands;
12 |
13 | using static Testing;
14 |
15 | public class UpdateTodoItemDetailTests : TestBase
16 | {
17 | [Test]
18 | public async Task ShouldRequireValidTodoItemId()
19 | {
20 | var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
21 | await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync();
22 | }
23 |
24 | [Test]
25 | public async Task ShouldUpdateTodoItem()
26 | {
27 | var userId = await RunAsDefaultUserAsync();
28 |
29 | var listId = await SendAsync(new CreateTodoListCommand
30 | {
31 | Title = "New List"
32 | });
33 |
34 | var itemId = await SendAsync(new CreateTodoItemCommand
35 | {
36 | ListId = listId,
37 | Title = "New Item"
38 | });
39 |
40 | var command = new UpdateTodoItemDetailCommand
41 | {
42 | Id = itemId,
43 | ListId = listId,
44 | Note = "This is the note.",
45 | Priority = PriorityLevel.High
46 | };
47 |
48 | await SendAsync(command);
49 |
50 | var item = await FindAsync(itemId);
51 |
52 | item.Should().NotBeNull();
53 | item!.ListId.Should().Be(command.ListId);
54 | item.Note.Should().Be(command.Note);
55 | item.Priority.Should().Be(command.Priority);
56 | item.LastModifiedBy.Should().NotBeNull();
57 | item.LastModifiedBy.Should().Be(userId);
58 | item.LastModified.Should().NotBeNull();
59 | item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoItems/Commands/UpdateTodoItemTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
3 | using CleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem;
4 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
5 | using CleanArchitecture.Domain.Entities;
6 | using FluentAssertions;
7 | using NUnit.Framework;
8 |
9 | namespace CleanArchitecture.Application.IntegrationTests.TodoItems.Commands;
10 |
11 | using static Testing;
12 |
13 | public class UpdateTodoItemTests : TestBase
14 | {
15 | [Test]
16 | public async Task ShouldRequireValidTodoItemId()
17 | {
18 | var command = new UpdateTodoItemCommand { Id = 99, Title = "New Title" };
19 | await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync();
20 | }
21 |
22 | [Test]
23 | public async Task ShouldUpdateTodoItem()
24 | {
25 | var userId = await RunAsDefaultUserAsync();
26 |
27 | var listId = await SendAsync(new CreateTodoListCommand
28 | {
29 | Title = "New List"
30 | });
31 |
32 | var itemId = await SendAsync(new CreateTodoItemCommand
33 | {
34 | ListId = listId,
35 | Title = "New Item"
36 | });
37 |
38 | var command = new UpdateTodoItemCommand
39 | {
40 | Id = itemId,
41 | Title = "Updated Item Title"
42 | };
43 |
44 | await SendAsync(command);
45 |
46 | var item = await FindAsync(itemId);
47 |
48 | item.Should().NotBeNull();
49 | item!.Title.Should().Be(command.Title);
50 | item.LastModifiedBy.Should().NotBeNull();
51 | item.LastModifiedBy.Should().Be(userId);
52 | item.LastModified.Should().NotBeNull();
53 | item.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoLists/Commands/CreateTodoListTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
3 | using CleanArchitecture.Domain.Entities;
4 | using FluentAssertions;
5 | using NUnit.Framework;
6 |
7 | namespace CleanArchitecture.Application.IntegrationTests.TodoLists.Commands;
8 |
9 | using static Testing;
10 |
11 | public class CreateTodoListTests : TestBase
12 | {
13 | [Test]
14 | public async Task ShouldRequireMinimumFields()
15 | {
16 | var command = new CreateTodoListCommand();
17 | await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync();
18 | }
19 |
20 | [Test]
21 | public async Task ShouldRequireUniqueTitle()
22 | {
23 | await SendAsync(new CreateTodoListCommand
24 | {
25 | Title = "Shopping"
26 | });
27 |
28 | var command = new CreateTodoListCommand
29 | {
30 | Title = "Shopping"
31 | };
32 |
33 | await FluentActions.Invoking(() =>
34 | SendAsync(command)).Should().ThrowAsync();
35 | }
36 |
37 | [Test]
38 | public async Task ShouldCreateTodoList()
39 | {
40 | var userId = await RunAsDefaultUserAsync();
41 |
42 | var command = new CreateTodoListCommand
43 | {
44 | Title = "Tasks"
45 | };
46 |
47 | var id = await SendAsync(command);
48 |
49 | var list = await FindAsync(id);
50 |
51 | list.Should().NotBeNull();
52 | list!.Title.Should().Be(command.Title);
53 | list.CreatedBy.Should().Be(userId);
54 | list.Created.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoLists/Commands/DeleteTodoListTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
3 | using CleanArchitecture.Application.TodoLists.Commands.DeleteTodoList;
4 | using CleanArchitecture.Domain.Entities;
5 | using FluentAssertions;
6 | using NUnit.Framework;
7 |
8 | namespace CleanArchitecture.Application.IntegrationTests.TodoLists.Commands;
9 |
10 | using static Testing;
11 |
12 | public class DeleteTodoListTests : TestBase
13 | {
14 | [Test]
15 | public async Task ShouldRequireValidTodoListId()
16 | {
17 | var command = new DeleteTodoListCommand { Id = 99 };
18 | await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync();
19 | }
20 |
21 | [Test]
22 | public async Task ShouldDeleteTodoList()
23 | {
24 | var listId = await SendAsync(new CreateTodoListCommand
25 | {
26 | Title = "New List"
27 | });
28 |
29 | await SendAsync(new DeleteTodoListCommand
30 | {
31 | Id = listId
32 | });
33 |
34 | var list = await FindAsync(listId);
35 |
36 | list.Should().BeNull();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoLists/Commands/PurgeTodoListsTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.Common.Security;
3 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
4 | using CleanArchitecture.Application.TodoLists.Commands.PurgeTodoLists;
5 | using CleanArchitecture.Domain.Entities;
6 | using FluentAssertions;
7 | using NUnit.Framework;
8 |
9 | namespace CleanArchitecture.Application.IntegrationTests.TodoLists.Commands;
10 |
11 | using static Testing;
12 |
13 | public class PurgeTodoListsTests : TestBase
14 | {
15 | [Test]
16 | public async Task ShouldDenyAnonymousUser()
17 | {
18 | var command = new PurgeTodoListsCommand();
19 |
20 | command.GetType().Should().BeDecoratedWith();
21 |
22 | await FluentActions.Invoking(() =>
23 | SendAsync(command)).Should().ThrowAsync();
24 | }
25 |
26 | [Test]
27 | public async Task ShouldDenyNonAdministrator()
28 | {
29 | await RunAsDefaultUserAsync();
30 |
31 | var command = new PurgeTodoListsCommand();
32 |
33 | await FluentActions.Invoking(() =>
34 | SendAsync(command)).Should().ThrowAsync();
35 | }
36 |
37 | [Test]
38 | public async Task ShouldAllowAdministrator()
39 | {
40 | await RunAsAdministratorAsync();
41 |
42 | var command = new PurgeTodoListsCommand();
43 |
44 | await FluentActions.Invoking(() => SendAsync(command))
45 | .Should().NotThrowAsync();
46 | }
47 |
48 | [Test]
49 | public async Task ShouldDeleteAllLists()
50 | {
51 | await RunAsAdministratorAsync();
52 |
53 | await SendAsync(new CreateTodoListCommand
54 | {
55 | Title = "New List #1"
56 | });
57 |
58 | await SendAsync(new CreateTodoListCommand
59 | {
60 | Title = "New List #2"
61 | });
62 |
63 | await SendAsync(new CreateTodoListCommand
64 | {
65 | Title = "New List #3"
66 | });
67 |
68 | await SendAsync(new PurgeTodoListsCommand());
69 |
70 | var count = await CountAsync();
71 |
72 | count.Should().Be(0);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoLists/Commands/UpdateTodoListTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using CleanArchitecture.Application.TodoLists.Commands.CreateTodoList;
3 | using CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList;
4 | using CleanArchitecture.Domain.Entities;
5 | using FluentAssertions;
6 | using NUnit.Framework;
7 |
8 | namespace CleanArchitecture.Application.IntegrationTests.TodoLists.Commands;
9 |
10 | using static Testing;
11 |
12 | public class UpdateTodoListTests : TestBase
13 | {
14 | [Test]
15 | public async Task ShouldRequireValidTodoListId()
16 | {
17 | var command = new UpdateTodoListCommand { Id = 99, Title = "New Title" };
18 | await FluentActions.Invoking(() => SendAsync(command)).Should().ThrowAsync();
19 | }
20 |
21 | [Test]
22 | public async Task ShouldRequireUniqueTitle()
23 | {
24 | var listId = await SendAsync(new CreateTodoListCommand
25 | {
26 | Title = "New List"
27 | });
28 |
29 | await SendAsync(new CreateTodoListCommand
30 | {
31 | Title = "Other List"
32 | });
33 |
34 | var command = new UpdateTodoListCommand
35 | {
36 | Id = listId,
37 | Title = "Other List"
38 | };
39 |
40 | (await FluentActions.Invoking(() =>
41 | SendAsync(command))
42 | .Should().ThrowAsync().Where(ex => ex.Errors.ContainsKey("Title")))
43 | .And.Errors["Title"].Should().Contain("The specified title already exists.");
44 | }
45 |
46 | [Test]
47 | public async Task ShouldUpdateTodoList()
48 | {
49 | var userId = await RunAsDefaultUserAsync();
50 |
51 | var listId = await SendAsync(new CreateTodoListCommand
52 | {
53 | Title = "New List"
54 | });
55 |
56 | var command = new UpdateTodoListCommand
57 | {
58 | Id = listId,
59 | Title = "Updated List Title"
60 | };
61 |
62 | await SendAsync(command);
63 |
64 | var list = await FindAsync(listId);
65 |
66 | list.Should().NotBeNull();
67 | list!.Title.Should().Be(command.Title);
68 | list.LastModifiedBy.Should().NotBeNull();
69 | list.LastModifiedBy.Should().Be(userId);
70 | list.LastModified.Should().NotBeNull();
71 | list.LastModified.Should().BeCloseTo(DateTime.Now, TimeSpan.FromMilliseconds(10000));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/TodoLists/Queries/GetTodosTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.TodoLists.Queries.GetTodos;
2 | using CleanArchitecture.Domain.Entities;
3 | using CleanArchitecture.Domain.ValueObjects;
4 | using FluentAssertions;
5 | using NUnit.Framework;
6 |
7 | namespace CleanArchitecture.Application.IntegrationTests.TodoLists.Queries;
8 |
9 | using static Testing;
10 |
11 | public class GetTodosTests : TestBase
12 | {
13 | [Test]
14 | public async Task ShouldReturnPriorityLevels()
15 | {
16 | var query = new GetTodosQuery();
17 |
18 | var result = await SendAsync(query);
19 |
20 | result.PriorityLevels.Should().NotBeEmpty();
21 | }
22 |
23 | [Test]
24 | public async Task ShouldReturnAllListsAndItems()
25 | {
26 | await AddAsync(new TodoList
27 | {
28 | Title = "Shopping",
29 | Colour = Colour.Blue,
30 | Items =
31 | {
32 | new TodoItem { Title = "Apples", Done = true },
33 | new TodoItem { Title = "Milk", Done = true },
34 | new TodoItem { Title = "Bread", Done = true },
35 | new TodoItem { Title = "Toilet paper" },
36 | new TodoItem { Title = "Pasta" },
37 | new TodoItem { Title = "Tissues" },
38 | new TodoItem { Title = "Tuna" }
39 | }
40 | });
41 |
42 | var query = new GetTodosQuery();
43 |
44 | var result = await SendAsync(query);
45 |
46 | result.Lists.Should().HaveCount(1);
47 | result.Lists.First().Items.Should().HaveCount(7);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Application.IntegrationTests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "UseInMemoryDatabase": false, // Application.IntegrationTests are not designed to work with InMemory database.
3 | "ConnectionStrings": {
4 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CleanArchitectureTestDb;Trusted_Connection=True;MultipleActiveResultSets=true;"
5 | },
6 | "IdentityServer": {
7 | "Clients": {
8 | "CleanArchitecture.WebUI": {
9 | "Profile": "IdentityServerSPA"
10 | }
11 | },
12 | "Key": {
13 | "Type": "Development"
14 | }
15 | },
16 | "Logging": {
17 | "LogLevel": {
18 | "Default": "Debug",
19 | "System": "Information",
20 | "Microsoft": "Information"
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/tests/Application.UnitTests/Application.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | CleanArchitecture.Application.UnitTests
6 | CleanArchitecture.Application.UnitTests
7 |
8 | false
9 | enable
10 | enable
11 |
12 |
13 |
14 |
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Behaviours;
2 | using CleanArchitecture.Application.Common.Interfaces;
3 | using CleanArchitecture.Application.TodoItems.Commands.CreateTodoItem;
4 | using Microsoft.Extensions.Logging;
5 | using Moq;
6 | using NUnit.Framework;
7 |
8 | namespace CleanArchitecture.Application.UnitTests.Common.Behaviours;
9 |
10 | public class RequestLoggerTests
11 | {
12 | private Mock> _logger = null!;
13 | private Mock _currentUserService = null!;
14 | private Mock _identityService = null!;
15 |
16 | [SetUp]
17 | public void Setup()
18 | {
19 | _logger = new Mock>();
20 | _currentUserService = new Mock();
21 | _identityService = new Mock();
22 | }
23 |
24 | [Test]
25 | public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated()
26 | {
27 | _currentUserService.Setup(x => x.UserId).Returns(Guid.NewGuid().ToString());
28 |
29 | var requestLogger = new LoggingBehaviour(_logger.Object, _currentUserService.Object, _identityService.Object);
30 |
31 | await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, new CancellationToken());
32 |
33 | _identityService.Verify(i => i.GetUserNameAsync(It.IsAny()), Times.Once);
34 | }
35 |
36 | [Test]
37 | public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated()
38 | {
39 | var requestLogger = new LoggingBehaviour(_logger.Object, _currentUserService.Object, _identityService.Object);
40 |
41 | await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, new CancellationToken());
42 |
43 | _identityService.Verify(i => i.GetUserNameAsync(It.IsAny()), Times.Never);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Application.UnitTests/Common/Exceptions/ValidationExceptionTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Common.Exceptions;
2 | using FluentAssertions;
3 | using FluentValidation.Results;
4 | using NUnit.Framework;
5 |
6 | namespace CleanArchitecture.Application.UnitTests.Common.Exceptions;
7 |
8 | public class ValidationExceptionTests
9 | {
10 | [Test]
11 | public void DefaultConstructorCreatesAnEmptyErrorDictionary()
12 | {
13 | var actual = new ValidationException().Errors;
14 |
15 | actual.Keys.Should().BeEquivalentTo(Array.Empty());
16 | }
17 |
18 | [Test]
19 | public void SingleValidationFailureCreatesASingleElementErrorDictionary()
20 | {
21 | var failures = new List
22 | {
23 | new ValidationFailure("Age", "must be over 18"),
24 | };
25 |
26 | var actual = new ValidationException(failures).Errors;
27 |
28 | actual.Keys.Should().BeEquivalentTo(new string[] { "Age" });
29 | actual["Age"].Should().BeEquivalentTo(new string[] { "must be over 18" });
30 | }
31 |
32 | [Test]
33 | public void MulitpleValidationFailureForMultiplePropertiesCreatesAMultipleElementErrorDictionaryEachWithMultipleValues()
34 | {
35 | var failures = new List
36 | {
37 | new ValidationFailure("Age", "must be 18 or older"),
38 | new ValidationFailure("Age", "must be 25 or younger"),
39 | new ValidationFailure("Password", "must contain at least 8 characters"),
40 | new ValidationFailure("Password", "must contain a digit"),
41 | new ValidationFailure("Password", "must contain upper case letter"),
42 | new ValidationFailure("Password", "must contain lower case letter"),
43 | };
44 |
45 | var actual = new ValidationException(failures).Errors;
46 |
47 | actual.Keys.Should().BeEquivalentTo(new string[] { "Password", "Age" });
48 |
49 | actual["Age"].Should().BeEquivalentTo(new string[]
50 | {
51 | "must be 25 or younger",
52 | "must be 18 or older",
53 | });
54 |
55 | actual["Password"].Should().BeEquivalentTo(new string[]
56 | {
57 | "must contain lower case letter",
58 | "must contain upper case letter",
59 | "must contain at least 8 characters",
60 | "must contain a digit",
61 | });
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/Application.UnitTests/Common/Mappings/MappingTests.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Serialization;
2 | using AutoMapper;
3 | using CleanArchitecture.Application.Common.Mappings;
4 | using CleanArchitecture.Application.TodoLists.Queries.GetTodos;
5 | using CleanArchitecture.Domain.Entities;
6 | using NUnit.Framework;
7 |
8 | namespace CleanArchitecture.Application.UnitTests.Common.Mappings;
9 |
10 | public class MappingTests
11 | {
12 | private readonly IConfigurationProvider _configuration;
13 | private readonly IMapper _mapper;
14 |
15 | public MappingTests()
16 | {
17 | _configuration = new MapperConfiguration(config =>
18 | config.AddProfile());
19 |
20 | _mapper = _configuration.CreateMapper();
21 | }
22 |
23 | [Test]
24 | public void ShouldHaveValidConfiguration()
25 | {
26 | _configuration.AssertConfigurationIsValid();
27 | }
28 |
29 | [Test]
30 | [TestCase(typeof(TodoList), typeof(TodoListDto))]
31 | [TestCase(typeof(TodoItem), typeof(TodoItemDto))]
32 | public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination)
33 | {
34 | var instance = GetInstanceOf(source);
35 |
36 | _mapper.Map(instance, source, destination);
37 | }
38 |
39 | private object GetInstanceOf(Type type)
40 | {
41 | if (type.GetConstructor(Type.EmptyTypes) != null)
42 | return Activator.CreateInstance(type)!;
43 |
44 | // Type without parameterless constructor
45 | return FormatterServices.GetUninitializedObject(type);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Domain.UnitTests/Domain.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |