,
88 | tooltip: 'Edit',
89 | onClick: (event, data) => console.log(data)
90 | }
91 | ]}
92 | options={{
93 | actionsColumnIndex: -1,
94 | exportButton: true,
95 | headerStyle: {
96 | fontWeight: "bold"
97 | }
98 | }}
99 | />
100 | )
101 | }
102 |
103 | export default ToDoDataTable;
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/helpers/api/ApiClientFactory.ts:
--------------------------------------------------------------------------------
1 | import { ToDoItemClient } from './Resources';
2 |
3 | export class ApiClientFactory {
4 | static GetToDoItemClient(): ToDoItemClient {
5 | const baseUrl : string = "https://localhost:5001";
6 | const client : ToDoItemClient = new ToDoItemClient(baseUrl);
7 |
8 | return client;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import './index.css';
5 | import App from './App';
6 | import reportWebVitals from './reportWebVitals';
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
15 | // If you want to start measuring performance in your app, pass a function
16 | // to log results (for example: reportWebVitals(console.log))
17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
18 | reportWebVitals();
19 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/layouts/shared/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ListItem from '@material-ui/core/ListItem';
3 | import ListItemIcon from '@material-ui/core/ListItemIcon';
4 | import ListItemText from '@material-ui/core/ListItemText';
5 | import ListSubheader from '@material-ui/core/ListSubheader';
6 | import DashboardIcon from '@material-ui/icons/Dashboard';
7 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
8 | import PeopleIcon from '@material-ui/icons/People';
9 | import BarChartIcon from '@material-ui/icons/BarChart';
10 | import LayersIcon from '@material-ui/icons/Layers';
11 | import AssignmentIcon from '@material-ui/icons/Assignment';
12 | import Divider from '@material-ui/core/Divider';
13 | import ListAltIcon from '@material-ui/icons/ListAlt';
14 |
15 | const Sidebar: React.FC = () => (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {/*
30 |
31 |
32 |
33 |
34 | */}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {/*
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Saved reports
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | */}
69 |
70 | );
71 |
72 |
73 | export default Sidebar;
74 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | }
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/views/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Typography from '@material-ui/core/Typography';
3 |
4 | const Dashboard : React.FC = () => {
5 | return (
6 | Dashboard
7 | )
8 |
9 | }
10 |
11 | export default Dashboard
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/src/views/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
3 | import { Link as RouterLink } from 'react-router-dom';
4 | import AddIcon from '@material-ui/icons/Add';
5 | import ToDoDataTable from '../components/ToDo/ToDoDataTable';
6 |
7 | // API
8 | import { Box, Button } from '@material-ui/core';
9 |
10 | const useStyles = makeStyles((theme: Theme) =>
11 | createStyles({
12 | boxroot: {
13 | width: '100%',
14 | backgroundColor: theme.palette.background.paper,
15 | padding: theme.spacing(2)
16 | },
17 | box: {
18 | display: "flex",
19 | justifyContent: "flex-end"
20 | },
21 | datatableRoot: {
22 | display: 'flex',
23 | height: '100%'
24 | },
25 | datatable: {
26 | flexGrow: 1
27 | }
28 | }),
29 | );
30 |
31 | const TodoList : React.FC = () => {
32 | const classes = useStyles();
33 |
34 | return (
35 | <>
36 |
37 |
38 | }
44 | >
45 | New
46 |
47 |
48 |
49 |
50 |
55 | >
56 |
57 | );
58 | }
59 |
60 | export default TodoList
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.ClientApp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/CleanArchitectureCosmosDB.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Constants/ApplicationIdentityConstants.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Core.Constants
2 | {
3 | public static class ApplicationIdentityConstants
4 | {
5 | public static class Roles
6 | {
7 | public static readonly string Administrator = "Administrator";
8 | public static readonly string Member = "Member";
9 |
10 | public static readonly string[] RolesSupported = { Administrator, Member };
11 | }
12 |
13 | public static readonly string DefaultPassword = "Password@1";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Entities/Audit/Audit.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities.Base;
2 | using System;
3 |
4 | namespace CleanArchitectureCosmosDB.Core.Entities
5 | {
6 | public class Audit : BaseEntity
7 | {
8 | public Audit(string entityType,
9 | string entityId,
10 | string entity)
11 | {
12 | this.EntityType = entityType;
13 | this.EntityId = entityId;
14 | this.Entity = entity;
15 | this.DateCreatedUTC = DateTime.UtcNow;
16 | }
17 |
18 | ///
19 | /// Type of the entity, e.g., ToDoItem
20 | ///
21 | public string EntityType { get; set; }
22 |
23 | ///
24 | /// Entity Id.
25 | /// Use this as the Partition Key, so that all the auditing records for the same entity are stored in the same logical partition.
26 | ///
27 | public string EntityId { get; set; }
28 |
29 | ///
30 | /// Entity itself
31 | ///
32 | public string Entity { get; set; }
33 |
34 | ///
35 | /// Date audit record created
36 | ///
37 | public DateTime DateCreatedUTC { get; set; }
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Entities/Base/BaseEntity.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Entities.Base
4 | {
5 | public abstract class BaseEntity
6 | {
7 | [JsonProperty(PropertyName = "id")]
8 | public virtual string Id { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Entities/ToDoItem.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities.Base;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Entities
4 | {
5 | public class ToDoItem : BaseEntity
6 | {
7 | ///
8 | /// Category which the To-Do-Item belongs to
9 | ///
10 | public string Category { get; set; }
11 | ///
12 | /// Title of the To-Do-Item
13 | ///
14 | public string Title { get; set; }
15 |
16 | ///
17 | /// Whether the To-Do-Item is done
18 | ///
19 | public bool IsCompleted { get; private set; }
20 |
21 | public void MarkComplete()
22 | {
23 | IsCompleted = true;
24 | }
25 |
26 | public override string ToString()
27 | {
28 | string status = IsCompleted ? "Done!" : "Not done.";
29 | return $"{Id}: Status: {status} - {Title}";
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Exceptions/EntityAlreadyExistsException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace CleanArchitectureCosmosDB.Core.Exceptions
6 | {
7 | public class EntityAlreadyExistsException : Exception
8 | {
9 | public EntityAlreadyExistsException() { }
10 |
11 | public EntityAlreadyExistsException(string message) : base(message) { }
12 |
13 | public EntityAlreadyExistsException(string message, Exception inner) : base(message, inner)
14 | { }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Exceptions/EntityNotFoundException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Exceptions
4 | {
5 | public class EntityNotFoundException : Exception
6 | {
7 | public EntityNotFoundException() { }
8 | public EntityNotFoundException(string message) : base(message) { }
9 | public EntityNotFoundException(string message, Exception innerException) : base(message, innerException)
10 | { }
11 |
12 | public EntityNotFoundException(string name, object key)
13 | : base($"Entity \"{name}\" ({key}) was not found.")
14 | {
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Exceptions/InvalidCredentialsException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace CleanArchitectureCosmosDB.Core.Exceptions
6 | {
7 | public class InvalidCredentialsException : Exception
8 | {
9 | public InvalidCredentialsException() : base("Invalid Username and/or Password. Please try again.")
10 | { }
11 | public InvalidCredentialsException(string message) : base(message) { }
12 | public InvalidCredentialsException(string message, Exception innerException) : base(message, innerException)
13 | { }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Cache/ICachedToDoItemsService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using System.Collections.Generic;
3 |
4 | namespace CleanArchitectureCosmosDB.Core.Interfaces
5 | {
6 | public interface ICachedToDoItemsService
7 | {
8 | IEnumerable GetCachedToDoItems();
9 | void DeleteCachedToDoItems();
10 | void SetCachedToDoItems(IEnumerable entry);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Email/IEmailService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Interfaces
4 | {
5 | public interface IEmailService
6 | {
7 | Task SendEmailAsync(string toEmail, string toName, string subject, string message);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IAuditRepository.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Interfaces.Persistence
4 | {
5 | public interface IAuditRepository : IRepository
6 | {
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IRepository.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 | using CleanArchitectureCosmosDB.Core.Entities.Base;
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 |
6 | namespace CleanArchitectureCosmosDB.Core.Interfaces
7 | {
8 | public interface IRepository where T : BaseEntity
9 | {
10 | ///
11 | /// Get items given a string SQL query directly.
12 | /// Likely in production, you may want to use alternatives like Parameterized Query or LINQ to avoid SQL Injection and avoid having to work with strings directly.
13 | /// This is kept here for demonstration purpose.
14 | ///
15 | ///
16 | ///
17 | Task> GetItemsAsync(string query);
18 | ///
19 | /// Get items given a specification.
20 | ///
21 | ///
22 | ///
23 | Task> GetItemsAsync(ISpecification specification);
24 |
25 | ///
26 | /// Get the count on items that match the specification
27 | ///
28 | ///
29 | ///
30 | Task GetItemsCountAsync(ISpecification specification);
31 |
32 | ///
33 | /// Get one item by Id
34 | ///
35 | ///
36 | ///
37 | Task GetItemAsync(string id);
38 | Task AddItemAsync(T item);
39 | Task UpdateItemAsync(string id, T item);
40 | Task DeleteItemAsync(string id);
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Persistence/IToDoItemRepository.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Interfaces
4 | {
5 | public interface IToDoItemRepository : IRepository
6 | {
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Interfaces/Storage/IStorageService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 |
5 | namespace CleanArchitectureCosmosDB.Core.Interfaces.Storage
6 | {
7 | public interface IStorageService
8 | {
9 | ///
10 | /// Upload a file and returns the full path to retrieve the file
11 | ///
12 | ///
13 | ///
14 | ///
15 | Task UploadFile(IFormFile file, string fullPath);
16 |
17 | ///
18 | /// Get the file stream by full path
19 | ///
20 | ///
21 | ///
22 | Task GetFileStream(string filePath);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/README.md:
--------------------------------------------------------------------------------
1 | # Core Project
2 |
3 | This project includes business entities and business logics in abstraction.
4 |
5 |
6 | * Application core defines the domain models and abstractions, such as interfaces, entities, domain services that do not belong to any specific entity, exceptions, domain events and handlers, specifications, etc.
7 | * Core project should have minimal dependencies, see dependencies in the project,
8 | particularly, application core should NOT depend on things like EF Core, SQL client, etc., since application core is only about high level business level logic, and should NOT care how things are implemented.
9 | * Infrastructure has dependency on application core, but not vice versa!
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/AuditFilterSpecification.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Specifications
4 | {
5 | public class AuditFilterSpecification : Specification
6 | {
7 | ///
8 | /// Search by a matching entity Id
9 | ///
10 | ///
11 | public AuditFilterSpecification(string entityId)
12 | {
13 | Query.Where(audit =>
14 | // Must include EntityId, because it is part of the Partition Key
15 | audit.EntityId == entityId)
16 | .OrderByDescending(audit => audit.DateCreatedUTC);
17 | }
18 |
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/Base/CosmosDbSpecificationEvaluator.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Specifications.Base
4 | {
5 | ///
6 | /// Specification Evaluator for Cosmos DB.
7 | /// The evaluator implements methods to translate specifications into Cosmos DB IQueryables, which then allows us to build queryables with filters, predicates etc. to query data.
8 | ///
9 | ///
10 | public class CosmosDbSpecificationEvaluator : SpecificationEvaluatorBase where T : class
11 | {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/Interfaces/ISearchQuery.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Core.Specifications.Interfaces
2 | {
3 | public interface ISearchQuery
4 | {
5 | int Start { get; set; }
6 | int PageSize { get; set; }
7 | string SortColumn { get; set; }
8 | SortDirection? SortDirection { get; set; }
9 | }
10 |
11 | public enum SortDirection
12 | {
13 | Ascending = 0,
14 | Descending = 1
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemGetAllSpecification.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 |
3 | namespace CleanArchitectureCosmosDB.Core.Specifications
4 | {
5 | public class ToDoItemGetAllSpecification : Specification
6 | {
7 | public ToDoItemGetAllSpecification(bool isCompleted)
8 | {
9 | // Use Specification Builder
10 | Query.Where(item =>
11 | item.IsCompleted == isCompleted
12 | );
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemSearchAggregationSpecification.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces;
3 |
4 | namespace CleanArchitectureCosmosDB.Core.Specifications
5 | {
6 | ///
7 | /// Specification for searching and returning aggregated value. E.g. Count, Sum, etc..
8 | /// This is similar to a search specification, minus the sorting.
9 | ///
10 | public class ToDoItemSearchAggregationSpecification : Specification
11 | {
12 | public ToDoItemSearchAggregationSpecification(string title = "",
13 | int pageStart = 0,
14 | int pageSize = 50,
15 | string sortColumn = "title",
16 | SortDirection sortDirection = SortDirection.Ascending,
17 | bool exactSearch = false
18 | )
19 | {
20 | if (!string.IsNullOrWhiteSpace(title))
21 | {
22 | if (exactSearch)
23 | {
24 | Query.Where(item => item.Title.ToLower() == title.ToLower());
25 | }
26 | else
27 | {
28 | Query.Where(item => item.Title.ToLower().Contains(title.ToLower()));
29 | }
30 | }
31 |
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Core/Specifications/ToDoItemSearchSpecification.cs:
--------------------------------------------------------------------------------
1 | using Ardalis.Specification;
2 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces;
3 |
4 | namespace CleanArchitectureCosmosDB.Core.Specifications
5 | {
6 | public class ToDoItemSearchSpecification : Specification
7 | {
8 | public ToDoItemSearchSpecification(string title = "",
9 | int pageStart = 0,
10 | int pageSize = 50,
11 | string sortColumn = "title",
12 | SortDirection sortDirection = SortDirection.Ascending,
13 | bool exactSearch = false
14 | )
15 | {
16 | if (!string.IsNullOrWhiteSpace(title))
17 | {
18 | if (exactSearch)
19 | {
20 | Query.Where(item => item.Title.ToLower() == title.ToLower());
21 | }
22 | else
23 | {
24 | Query.Where(item => item.Title.ToLower().Contains(title.ToLower()));
25 | }
26 | }
27 |
28 | // Pagination
29 | if (pageSize != -1) //Display all entries and disable pagination
30 | {
31 | Query.Skip(pageStart).Take(pageSize);
32 | }
33 |
34 | // Sort
35 | switch (sortColumn.ToLower())
36 | {
37 | case ("title"):
38 | {
39 | if (sortDirection == SortDirection.Ascending)
40 | {
41 | Query.OrderBy(x => x.Title);
42 | }
43 | else
44 | {
45 | Query.OrderByDescending(x => x.Title);
46 | }
47 | }
48 | break;
49 | default:
50 | break;
51 | }
52 |
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/AppSettings/CosmosDbSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace CleanArchitectureCosmosDB.Infrastructure.AppSettings
4 | {
5 | public class CosmosDbSettings
6 | {
7 | ///
8 | /// CosmosDb Account - The Azure Cosmos DB endpoint
9 | ///
10 | public string EndpointUrl { get; set; }
11 | ///
12 | /// Key - The primary key for the Azure DocumentDB account.
13 | ///
14 | public string PrimaryKey { get; set; }
15 | ///
16 | /// Database name
17 | ///
18 | public string DatabaseName { get; set; }
19 |
20 | ///
21 | /// List of containers in the database
22 | ///
23 | public List Containers { get; set; }
24 |
25 | }
26 | public class ContainerInfo
27 | {
28 | ///
29 | /// Container Name
30 | ///
31 | public string Name { get; set; }
32 | ///
33 | /// Container partition Key
34 | ///
35 | public string PartitionKey { get; set; }
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/AppSettings/SendGridEmailSettings.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Infrastructure.AppSettings
2 | {
3 | ///
4 | /// SendGrid email settings
5 | ///
6 | public class SendGridEmailSettings
7 | {
8 | ///
9 | /// API Key
10 | ///
11 | public string SendGridApiKey { get; set; }
12 | ///
13 | /// From Email
14 | ///
15 | public string FromEmail { get; set; }
16 | ///
17 | /// From Name
18 | ///
19 | public string FromName { get; set; }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CleanArchitectureCosmosDB.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Constants/CosmosDbConfigConstants.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Constants
2 | {
3 | public class CosmosDbContainerConstants
4 | {
5 | // TODO : consider retrieving this from appsettings using IOptions, instead of defining it as a constant
6 | public const string CONTAINER_NAME_TODO = "Todo";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/CosmosDbContainer.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
2 | using Microsoft.Azure.Cosmos;
3 |
4 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData
5 | {
6 | public class CosmosDbContainer : ICosmosDbContainer
7 | {
8 | public Container _container { get; }
9 |
10 | public CosmosDbContainer(CosmosClient cosmosClient,
11 | string databaseName,
12 | string containerName)
13 | {
14 | this._container = cosmosClient.GetContainer(databaseName, containerName);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/CosmosDbContainerFactory.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings;
2 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
3 | using Microsoft.Azure.Cosmos;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 |
9 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData
10 | {
11 | public class CosmosDbContainerFactory : ICosmosDbContainerFactory
12 | {
13 | ///
14 | /// Azure Cosmos DB Client
15 | ///
16 | private readonly CosmosClient _cosmosClient;
17 | private readonly string _databaseName;
18 | private readonly List _containers;
19 |
20 | ///
21 | /// Ctor
22 | ///
23 | ///
24 | ///
25 | ///
26 | public CosmosDbContainerFactory(CosmosClient cosmosClient,
27 | string databaseName,
28 | List containers)
29 | {
30 | _databaseName = databaseName ?? throw new ArgumentNullException(nameof(databaseName));
31 | _containers = containers ?? throw new ArgumentNullException(nameof(containers));
32 | _cosmosClient = cosmosClient ?? throw new ArgumentNullException(nameof(cosmosClient));
33 | }
34 |
35 | public ICosmosDbContainer GetContainer(string containerName)
36 | {
37 | if (_containers.Where(x => x.Name == containerName) == null)
38 | {
39 | throw new ArgumentException($"Unable to find container: {containerName}");
40 | }
41 |
42 | return new CosmosDbContainer(_cosmosClient, _databaseName, containerName);
43 | }
44 |
45 | public async Task EnsureDbSetupAsync()
46 | {
47 | Microsoft.Azure.Cosmos.DatabaseResponse database = await _cosmosClient.CreateDatabaseIfNotExistsAsync(_databaseName);
48 |
49 | foreach (ContainerInfo container in _containers)
50 | {
51 | await database.Database.CreateContainerIfNotExistsAsync(container.Name, $"{container.PartitionKey}");
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/IContainerContext.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities.Base;
2 | using Microsoft.Azure.Cosmos;
3 |
4 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces
5 | {
6 | ///
7 | /// Defines the container level context
8 | ///
9 | ///
10 | public interface IContainerContext where T : BaseEntity
11 | {
12 | string ContainerName { get; }
13 | string GenerateId(T entity);
14 | PartitionKey ResolvePartitionKey(string entityId);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/ICosmosDbContainer.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Cosmos;
2 |
3 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces
4 | {
5 | public interface ICosmosDbContainer
6 | {
7 | ///
8 | /// Instance of Azure Cosmos DB Container class
9 | ///
10 | Container _container { get; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Interfaces/ICosmosDbContainerFactory.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces
4 | {
5 | public interface ICosmosDbContainerFactory
6 | {
7 | ///
8 | /// Returns a CosmosDbContainer wrapper
9 | ///
10 | ///
11 | ///
12 | ICosmosDbContainer GetContainer(string containerName);
13 |
14 | ///
15 | /// Ensure the database is created
16 | ///
17 | ///
18 | Task EnsureDbSetupAsync();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Repository/AuditRepository.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence;
3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
4 | using Microsoft.Azure.Cosmos;
5 |
6 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository
7 | {
8 | ///
9 | /// Audit repository
10 | ///
11 | public class AuditRepository : CosmosDbRepository, IAuditRepository
12 | {
13 | ///
14 | /// Name of the cosmosDb container where entity records will reside.
15 | ///
16 | public override string ContainerName { get; } = "Audit";
17 | public override string GenerateId(Audit entity) => GenerateAuditId(entity);
18 | public override PartitionKey ResolvePartitionKey(string entityId) => ResolveAuditPartitionKey(entityId);
19 |
20 | public AuditRepository(ICosmosDbContainerFactory factory) : base(factory)
21 | { }
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/CosmosDbData/Repository/ToDoItemRepository.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
4 | using Microsoft.Azure.Cosmos;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Threading.Tasks;
8 |
9 | namespace CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository
10 | {
11 | public class ToDoItemRepository : CosmosDbRepository, IToDoItemRepository
12 | {
13 | ///
14 | /// CosmosDB container name
15 | ///
16 | public override string ContainerName { get; } = "Todo";
17 |
18 | ///
19 | /// Generate Id.
20 | /// e.g. "shoppinglist:783dfe25-7ece-4f0b-885e-c0ea72135942"
21 | ///
22 | ///
23 | ///
24 | public override string GenerateId(ToDoItem entity) => $"{entity.Category}:{Guid.NewGuid()}";
25 |
26 | ///
27 | /// Returns the value of the partition key
28 | ///
29 | ///
30 | ///
31 | public override PartitionKey ResolvePartitionKey(string entityId) => new PartitionKey(entityId.Split(':')[0]);
32 |
33 | public ToDoItemRepository(ICosmosDbContainerFactory factory) : base(factory)
34 | { }
35 |
36 | // Use Cosmos DB Parameterized Query to avoid SQL Injection.
37 | // Get by Category is also an example of single partition read, where get by title will be a cross partition read
38 | public async Task> GetItemsAsyncByCategory(string category)
39 | {
40 | List results = new List();
41 | string query = @$"SELECT c.Name FROM c WHERE c.Category = @Category";
42 |
43 | QueryDefinition queryDefinition = new QueryDefinition(query)
44 | .WithParameter("@Category", category);
45 | string queryString = queryDefinition.QueryText;
46 |
47 | IEnumerable entities = await this.GetItemsAsync(queryString);
48 |
49 | return results;
50 | }
51 |
52 | // Use Cosmos DB Parameterized Query to avoid SQL Injection.
53 | // Get by Title is also an example of cross partition read, where Get by Category will be single partition read
54 | public async Task> GetItemsAsyncByTitle(string title)
55 | {
56 | List results = new List();
57 | string query = @$"SELECT c.Name FROM c WHERE c.Title = @Title";
58 |
59 | QueryDefinition queryDefinition = new QueryDefinition(query)
60 | .WithParameter("@Title", title);
61 | string queryString = queryDefinition.QueryText;
62 |
63 | IEnumerable entities = await this.GetItemsAsync(queryString);
64 |
65 | return results;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Extensions/CacheHelpers.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions
2 | {
3 | public static class CacheHelpers
4 | {
5 | public static string GenerateToDoItemsCacheKey()
6 | {
7 | return "todoitems";
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Extensions/IApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Identity;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions
13 | {
14 | ///
15 | /// Extension methods for IApplicationBuilder
16 | ///
17 | public static class IApplicationBuilderExtensions
18 | {
19 | ///
20 | /// Ensure Cosmos DB is created
21 | ///
22 | ///
23 | public static void EnsureCosmosDbIsCreated(this IApplicationBuilder builder)
24 | {
25 | using (IServiceScope serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope())
26 | {
27 | ICosmosDbContainerFactory factory = serviceScope.ServiceProvider.GetService();
28 |
29 | factory.EnsureDbSetupAsync().Wait();
30 | }
31 | }
32 |
33 | ///
34 | /// Seed sample data in the Todo container
35 | ///
36 | ///
37 | ///
38 | public static async Task SeedToDoContainerIfEmptyAsync(this IApplicationBuilder builder)
39 | {
40 | using (IServiceScope serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope())
41 | {
42 | IToDoItemRepository _repo = serviceScope.ServiceProvider.GetService();
43 |
44 | // Check if empty
45 | string sqlQueryText = "SELECT * FROM c";
46 | IEnumerable todos = await _repo.GetItemsAsync(sqlQueryText);
47 |
48 | if (todos.Count() == 0)
49 | {
50 | for (int i = 0; i < 100; i++)
51 | {
52 | ToDoItem beer = new ToDoItem()
53 | {
54 | Category = "Grocery",
55 | Title = $"Get {i} beers"
56 | };
57 |
58 | await _repo.AddItemAsync(beer);
59 | }
60 | }
61 | }
62 | }
63 |
64 | ///
65 | /// Create Identity DB if not exist
66 | ///
67 | ///
68 | public static void EnsureIdentityDbIsCreated(this IApplicationBuilder builder)
69 | {
70 | using (var serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope())
71 | {
72 | var services = serviceScope.ServiceProvider;
73 |
74 | var dbContext = services.GetRequiredService();
75 |
76 | // Ensure the database is created.
77 | // Note this does not use migrations. If database may be updated using migrations, use DbContext.Database.Migrate() instead.
78 | dbContext.Database.EnsureCreated();
79 | }
80 | }
81 |
82 | ///
83 | /// Seed Identity data
84 | ///
85 | ///
86 | public static async Task SeedIdentityDataAsync(this IApplicationBuilder builder)
87 | {
88 | using (var serviceScope = builder.ApplicationServices.GetRequiredService().CreateScope())
89 | {
90 | var services = serviceScope.ServiceProvider;
91 |
92 | var userManager = services.GetRequiredService>();
93 | var roleManager = services.GetRequiredService>();
94 |
95 | await Infrastructure.Identity.Seed.ApplicationDbContextDataSeed.SeedAsync(userManager, roleManager);
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Extensions/IServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage;
2 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings;
3 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData;
4 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Interfaces;
5 | using CleanArchitectureCosmosDB.Infrastructure.Services;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Storage.Net;
9 | using System.Collections.Generic;
10 |
11 | namespace CleanArchitectureCosmosDB.Infrastructure.Extensions
12 | {
13 | public static class IServiceCollectionExtensions
14 | {
15 | ///
16 | /// Register a singleton instance of Cosmos Db Container Factory, which is a wrapper for the CosmosClient.
17 | ///
18 | ///
19 | ///
20 | ///
21 | ///
22 | ///
23 | ///
24 | public static IServiceCollection AddCosmosDb(this IServiceCollection services,
25 | string endpointUrl,
26 | string primaryKey,
27 | string databaseName,
28 | List containers)
29 | {
30 | Microsoft.Azure.Cosmos.CosmosClient client = new Microsoft.Azure.Cosmos.CosmosClient(endpointUrl, primaryKey);
31 | CosmosDbContainerFactory cosmosDbClientFactory = new CosmosDbContainerFactory(client, databaseName, containers);
32 |
33 | // Microsoft recommends a singleton client instance to be used throughout the application
34 | // https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.cosmosclient?view=azure-dotnet#definition
35 | // "CosmosClient is thread-safe. Its recommended to maintain a single instance of CosmosClient per lifetime of the application which enables efficient connection management and performance"
36 | services.AddSingleton(cosmosDbClientFactory);
37 |
38 | return services;
39 | }
40 |
41 | ///
42 | /// Setup Azure Blob storage
43 | ///
44 | ///
45 | ///
46 | public static void SetupStorage(this IServiceCollection services, IConfiguration configuration)
47 | {
48 | StorageFactory.Modules.UseAzureBlobStorage();
49 |
50 | // Register IBlobStorage, which is used in AzureBlobStorageService
51 | // Avoid using IBlobStorage directly outside of AzureBlobStorageService.
52 | services.AddScoped(
53 | factory => StorageFactory.Blobs.FromConnectionString(configuration.GetConnectionString("StorageConnectionString")));
54 |
55 | services.AddScoped();
56 | }
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/DesignTime/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System;
3 |
4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models
5 | {
6 | public partial class ApplicationDbContext
7 | {
8 | // for checking that DI is getting a different instance each time when the dbcontext is injected in the context of a web request
9 | private Guid _instanceId = Guid.NewGuid();
10 |
11 | public static void AddBaseOptions(DbContextOptionsBuilder builder, string connectionString)
12 | {
13 | if (builder == null)
14 | throw new ArgumentNullException(nameof(builder));
15 |
16 | if (string.IsNullOrWhiteSpace(connectionString))
17 | throw new ArgumentException("Connection string must be provided", nameof(connectionString));
18 |
19 | builder.UseSqlServer(connectionString, x =>
20 | {
21 | x.EnableRetryOnFailure();
22 | });
23 | }
24 |
25 | public static void AddBaseOptions(DbContextOptionsBuilder builder, string connectionString)
26 | {
27 | if (builder == null)
28 | throw new ArgumentNullException(nameof(builder));
29 |
30 | if (string.IsNullOrWhiteSpace(connectionString))
31 | throw new ArgumentException("Connection string must be provided", nameof(connectionString));
32 |
33 | builder.UseSqlServer(connectionString, x =>
34 | {
35 | x.EnableRetryOnFailure();
36 | });
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/DesignTime/DesignTimeDbContextFactory.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.EntityFrameworkCore.Design;
4 | using Microsoft.Extensions.Configuration;
5 | using System;
6 | using System.IO;
7 |
8 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.DesignTime
9 | {
10 | ///
11 | /// Used for design time migrations. Will look to the appsettings.json file in this project
12 | /// for the connection string.
13 | /// EF Core tools scans the assembly containing the dbcontext for an implementation
14 | /// of IDesignTimeDbContextFactory.
15 | ///
16 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory
17 | {
18 | public ApplicationDbContext CreateDbContext(string[] args)
19 | {
20 | string path = Directory.GetCurrentDirectory();
21 |
22 | IConfigurationBuilder builder =
23 | new ConfigurationBuilder()
24 | .SetBasePath(path)
25 | .AddJsonFile("appsettings.json");
26 |
27 | IConfigurationRoot config = builder.Build();
28 |
29 | string connectionString = config.GetConnectionString("CleanArchitectureIdentity");
30 |
31 | Console.WriteLine($"DesignTimeDbContextFactory: using base path = {path}");
32 | Console.WriteLine($"DesignTimeDbContextFactory: using connection string = {connectionString}");
33 |
34 | if (string.IsNullOrWhiteSpace(connectionString))
35 | {
36 | throw new InvalidOperationException("Could not find connection string named 'CleanArchitectureIdentity'");
37 | }
38 |
39 | DbContextOptionsBuilder dbContextOptionsBuilder =
40 | new DbContextOptionsBuilder();
41 |
42 | ApplicationDbContext.AddBaseOptions(dbContextOptionsBuilder, connectionString);
43 |
44 | return new ApplicationDbContext(dbContextOptionsBuilder.Options);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models
5 | {
6 | public partial class ApplicationDbContext : IdentityDbContext
7 | {
8 | public ApplicationDbContext(DbContextOptions options)
9 | : base(options)
10 | { }
11 |
12 | protected override void OnModelCreating(ModelBuilder builder)
13 | {
14 | base.OnModelCreating(builder);
15 |
16 | // Customize the ASP.NET Identity model and override the defaults if needed.
17 | // For example, you can rename the ASP.NET Identity table names and more.
18 | // Add your customizations after calling base.OnModelCreating(builder);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/ApplicationUser.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 | using System.Collections.Generic;
3 | using System.Runtime.Serialization;
4 |
5 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models
6 | {
7 | public class ApplicationUser : IdentityUser
8 | {
9 | public string FirstName { get; set; }
10 | public string LastName { get; set; }
11 | public bool IsEnabled { get; set; }
12 |
13 | [IgnoreDataMember]
14 | public string FullName
15 | {
16 | get
17 | {
18 | return $"{FirstName} {LastName}";
19 | }
20 | }
21 |
22 | //[JsonIgnore]
23 | public List RefreshTokens { get; set; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/Token.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication
4 | {
5 | [JsonObject("token")]
6 | public class Token
7 | {
8 | [JsonProperty("secret")]
9 | public string Secret { get; set; }
10 |
11 | [JsonProperty("issuer")]
12 | public string Issuer { get; set; }
13 |
14 | [JsonProperty("audience")]
15 | public string Audience { get; set; }
16 |
17 | [JsonProperty("expiry")]
18 | public int Expiry { get; set; }
19 |
20 | [JsonProperty("refreshExpiry")]
21 | public int RefreshExpiry { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/TokenRequest.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication
5 | {
6 | public class TokenRequest
7 | {
8 | ///
9 | /// The username of the user logging in.
10 | ///
11 | [Required]
12 | [JsonProperty("username")]
13 | public string Username { get; set; }
14 |
15 | ///
16 | /// The password for the user logging in.
17 | ///
18 | [Required]
19 | [JsonProperty("password")]
20 | public string Password { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/Authentication/TokenResponse.cs:
--------------------------------------------------------------------------------
1 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication
2 | {
3 | public class TokenResponse
4 | {
5 | public TokenResponse(ApplicationUser user,
6 | string role,
7 | string token
8 | //string refreshToken
9 | )
10 | {
11 | Id = user.Id;
12 | FullName = user.FullName;
13 | EmailAddress = user.Email;
14 | Token = token;
15 | Role = role;
16 | //RefreshToken = refreshToken;
17 | }
18 |
19 | public string Id { get; set; }
20 | public string FullName { get; set; }
21 | public string EmailAddress { get; set; }
22 | public string Token { get; set; }
23 | public string Role { get; set; }
24 |
25 | //[JsonIgnore]
26 | //public string RefreshToken { get; set; }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Models/RefreshToken.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.ComponentModel.DataAnnotations;
6 | using System.Text;
7 |
8 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Models
9 | {
10 | [Owned]
11 | public class RefreshToken
12 | {
13 | [Key]
14 | [JsonIgnore]
15 | public int Id { get; set; }
16 |
17 | public string Token { get; set; }
18 | public DateTime Expiry { get; set; }
19 | public bool IsExpired
20 | {
21 | get { return DateTime.UtcNow >= Expiry; }
22 | }
23 | public DateTime Created { get; set; }
24 | public string CreatedByIp { get; set; }
25 | public DateTime? Revoked { get; set; }
26 | public string RevokedByIp { get; set; }
27 | public string ReplacedByToken { get; set; }
28 |
29 | public bool IsActive
30 | {
31 | get { return Revoked == null && !IsExpired; }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/README_Identity.md:
--------------------------------------------------------------------------------
1 | # ASP.NET Core Identity is used to provide token service and user management service.
2 |
3 | # Entity Framework Core
4 | ## NOTES:
5 | 1. migrations will use the connection string defined in CleanArchitectureCosmosDB.Infrastructure/appsettings.json
6 | as this is the connection string referenced by DesignTimeDbContextFactory
7 | 1. Consult Microsoft documentation on Entity Framework Core Code First migrations for more information on migrations.
8 |
9 | ## To create migrations for the first time:
10 | * Add-Migration -Name "InitialIdentityDbCreation" -OutputDir "Identity\Migrations" -Context "CleanArchitectureCosmosDB.Infrastructure.Identity.Models.ApplicationDbContext" -Project "CleanArchitectureCosmosDB.Infrastructure"
11 |
12 | ## To run the migrations:
13 | * Update-Database -Context "CleanArchitectureCosmosDB.Infrastructure.Identity.Models.ApplicationDbContext" -Project "CleanArchitectureCosmosDB.Infrastructure"
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Seed/ApplicationDbContextDataSeed.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Constants;
2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
3 | using Microsoft.AspNetCore.Identity;
4 | using System.Threading.Tasks;
5 |
6 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Seed
7 | {
8 | public class ApplicationDbContextDataSeed
9 | {
10 | ///
11 | /// Seed users and roles in the Identity database.
12 | ///
13 | /// ASP.NET Core Identity User Manager
14 | /// ASP.NET Core Identity Role Manager
15 | ///
16 | public static async Task SeedAsync(UserManager userManager, RoleManager roleManager)
17 | {
18 | // Add roles supported
19 | await roleManager.CreateAsync(new IdentityRole(ApplicationIdentityConstants.Roles.Administrator));
20 | await roleManager.CreateAsync(new IdentityRole(ApplicationIdentityConstants.Roles.Member));
21 |
22 | // New admin user
23 | string adminUserName = "shawn@test.com";
24 | var adminUser = new ApplicationUser {
25 | UserName = adminUserName,
26 | Email = adminUserName,
27 | IsEnabled = true,
28 | EmailConfirmed = true,
29 | FirstName = "Shawn",
30 | LastName = "Administrator"
31 | };
32 |
33 | // Add new user and their role
34 | await userManager.CreateAsync(adminUser, ApplicationIdentityConstants.DefaultPassword);
35 | adminUser = await userManager.FindByNameAsync(adminUserName);
36 | await userManager.AddToRoleAsync(adminUser, ApplicationIdentityConstants.Roles.Administrator);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Services/ITokenService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
3 | using System.Threading.Tasks;
4 |
5 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Services
6 | {
7 | ///
8 | /// A collection of token related services
9 | ///
10 | public interface ITokenService
11 | {
12 | ///
13 | /// Validate the credentials entered when logging in.
14 | ///
15 | ///
16 | ///
17 | ///
18 | Task Authenticate(TokenRequest request, string ipAddress);
19 |
20 | ///
21 | /// If the refresh token is valid, a new JWT token will be issued containing the user details.
22 | ///
23 | /// An existing refresh token.
24 | /// The users current ip
25 | ///
26 | ///
27 | ///
28 | Task RefreshToken(string refreshToken, string ipAddress);
29 |
30 |
31 | ///
32 | /// Check if the credentials passed in are valid.
33 | ///
34 | /// The username to check.
35 | /// The matching password to verify.
36 | /// If the credentials are valid or not.
37 | Task IsValidUser(string username, string password);
38 |
39 | ///
40 | /// Find an by their email.
41 | ///
42 | ///
43 | ///
44 | ///
45 | ///
46 | ///
47 | ///
48 | Task GetUserByEmail(string email);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/Services/TokenService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
2 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.AspNetCore.Identity;
5 | using Microsoft.Extensions.Options;
6 | using Microsoft.IdentityModel.Tokens;
7 | using System;
8 | using System.IdentityModel.Tokens.Jwt;
9 | using System.Security.Claims;
10 | using System.Text;
11 | using System.Threading.Tasks;
12 |
13 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity.Services
14 | {
15 | ///
16 | public class TokenService : ITokenService
17 | {
18 | private readonly SignInManager _signInManager;
19 | private readonly UserManager _userManager;
20 | private readonly Token _token;
21 | private readonly HttpContext _httpContext;
22 |
23 | ///
24 | public TokenService(
25 | UserManager userManager,
26 | SignInManager signInManager,
27 | IOptions tokenOptions,
28 | IHttpContextAccessor httpContextAccessor)
29 | {
30 | _userManager = userManager;
31 | _signInManager = signInManager;
32 | _token = tokenOptions.Value;
33 | _httpContext = httpContextAccessor.HttpContext;
34 | }
35 |
36 | ///
37 | public async Task Authenticate(TokenRequest request, string ipAddress)
38 | {
39 | if (await IsValidUser(request.Username, request.Password))
40 | {
41 | ApplicationUser user = await GetUserByEmail(request.Username);
42 |
43 | if (user != null && user.IsEnabled)
44 | {
45 | string role = (await _userManager.GetRolesAsync(user))[0];
46 | string jwtToken = await GenerateJwtToken(user);
47 |
48 | //RefreshToken refreshToken = GenerateRefreshToken(ipAddress);
49 |
50 | //user.RefreshTokens.Add(refreshToken);
51 | await _userManager.UpdateAsync(user);
52 |
53 | return new TokenResponse(user,
54 | role,
55 | jwtToken
56 | //""//refreshToken.Token
57 | );
58 | }
59 | }
60 |
61 | return null;
62 | }
63 |
64 | public Task RefreshToken(string refreshToken, string ipAddress)
65 | {
66 | throw new NotImplementedException();
67 | }
68 |
69 | ///
70 | public async Task IsValidUser(string username, string password)
71 | {
72 | ApplicationUser user = await GetUserByEmail(username);
73 |
74 | if (user == null)
75 | {
76 | // Username or password was incorrect.
77 | return false;
78 | }
79 |
80 | SignInResult signInResult = await _signInManager.PasswordSignInAsync(user, password, true, false);
81 |
82 | return signInResult.Succeeded;
83 | }
84 |
85 | ///
86 | public async Task GetUserByEmail(string email)
87 | {
88 | return await _userManager.FindByEmailAsync(email);
89 | }
90 |
91 | ///
92 | /// Issue JWT token
93 | ///
94 | ///
95 | ///
96 | private async Task GenerateJwtToken(ApplicationUser user)
97 | {
98 | string role = (await _userManager.GetRolesAsync(user))[0];
99 | byte[] secret = Encoding.ASCII.GetBytes(_token.Secret);
100 |
101 | JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
102 | SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
103 | {
104 | Issuer = _token.Issuer,
105 | Audience = _token.Audience,
106 | Subject = new ClaimsIdentity(new Claim[]
107 | {
108 | new Claim("UserId", user.Id),
109 | new Claim("FullName", $"{user.FirstName} {user.LastName}"),
110 | new Claim(ClaimTypes.Name, user.Email),
111 | new Claim(ClaimTypes.NameIdentifier, user.Email),
112 | new Claim(ClaimTypes.Role, role)
113 | }),
114 | Expires = DateTime.UtcNow.AddMinutes(_token.Expiry),
115 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(secret), SecurityAlgorithms.HmacSha256Signature)
116 | };
117 |
118 | SecurityToken token = handler.CreateToken(descriptor);
119 | return handler.WriteToken(token);
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Identity/TokenServiceProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 |
6 | namespace CleanArchitectureCosmosDB.Infrastructure.Identity
7 | {
8 | ///
9 | /// Config settings for token service provider.
10 | /// E.g., application using ASP.NET Core Identity, Identity Server, etc..
11 | ///
12 | public class TokenServiceProvider
13 | {
14 | public string Authority { get; set; }
15 | public string SetPasswordPath { get; set; }
16 | public string ResetPasswordPath { get; set; }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/README.md:
--------------------------------------------------------------------------------
1 | # Infrastructure
2 |
3 | This project has plumbing code that implements the abstractions defined in Core project.
4 |
5 | * Infrastructure has plumbing code, such as repositories, EF Core DbContext if used, Cached repositories, third party APIs, file systems, email service implementations, logging adapters, third party SDKs like S3 or Azure Blob Storage SDKs.
6 | * Infrastructure has dependency on application core, but not vice versa!
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Services/AzureBlobStorageService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage;
2 | using Microsoft.AspNetCore.Http;
3 | using Storage.Net.Blobs;
4 | using System;
5 | using System.IO;
6 | using System.Threading.Tasks;
7 |
8 | namespace CleanArchitectureCosmosDB.Infrastructure.Services
9 | {
10 | ///
11 | /// Azure Blob Storage
12 | ///
13 | public class AzureBlobStorageService : IStorageService
14 | {
15 | private readonly IBlobStorage _blobStorage;
16 |
17 | public AzureBlobStorageService(IBlobStorage blobStorage)
18 | {
19 | _blobStorage = blobStorage ?? throw new ArgumentNullException(nameof(blobStorage));
20 | }
21 |
22 | public async Task UploadFile(IFormFile file, string fullPath)
23 | {
24 | using (Stream str = file.OpenReadStream())
25 | {
26 | await _blobStorage.WriteAsync(fullPath, str, false);
27 | }
28 |
29 | return fullPath;
30 | }
31 |
32 | public async Task GetFileStream(string filePath)
33 | {
34 | MemoryStream ms = new MemoryStream();
35 | await _blobStorage.ReadToStreamAsync(filePath, ms);
36 | return ms;
37 | }
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/Services/SendGridEmailService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Interfaces;
2 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings;
3 | using Microsoft.Extensions.Options;
4 | using SendGrid;
5 | using SendGrid.Helpers.Mail;
6 | using System;
7 | using System.Threading.Tasks;
8 |
9 | namespace CleanArchitectureCosmosDB.Infrastructure.Services
10 | {
11 | public class SendGridEmailService : IEmailService
12 | {
13 | ///
14 | /// Settings
15 | ///
16 | private readonly SendGridEmailSettings _sendGridEmailSettings;
17 |
18 | ///
19 | /// Send Grid wrapper
20 | ///
21 | private readonly SendGridClient _sendGridClient;
22 |
23 | ///
24 | /// FromEmail from the settings
25 | ///
26 | private string FromEmail => _sendGridEmailSettings.FromEmail;
27 |
28 | ///
29 | /// FromName from the settings
30 | ///
31 | private string FromName => _sendGridEmailSettings.FromName;
32 |
33 | ///
34 | /// ctor
35 | ///
36 | ///
37 | public SendGridEmailService(IOptions sendGridEmailSettings)
38 | {
39 | _sendGridEmailSettings = sendGridEmailSettings.Value ?? throw new ArgumentNullException(nameof(sendGridEmailSettings));
40 | _sendGridClient = new SendGridClient(_sendGridEmailSettings.SendGridApiKey);
41 |
42 | }
43 |
44 | // TODO : consider adding support for HTML content
45 | ///
46 | /// Send message
47 | ///
48 | ///
49 | ///
50 | ///
51 | ///
52 | ///
53 | public async Task SendEmailAsync(string toEmail, string toName, string subject, string message)
54 | {
55 | SendGridMessage sendGridMessage = MailHelper.CreateSingleEmail(
56 | new EmailAddress(this.FromEmail, this.FromName),
57 | new EmailAddress(toEmail, toName),
58 | subject,
59 | message,
60 | message
61 | );
62 |
63 | await _sendGridClient.SendEmailAsync(sendGridMessage);
64 | }
65 | }
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.Infrastructure/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "CleanArchitectureIdentity": "Server=localhost\\SQLSERVER2016;Database=CleanArchitectureIdentity;Trusted_Connection=True;"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/CleanArchitectureCosmosDB.WebAPI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/AuthenticationConfig.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
2 | using Microsoft.AspNetCore.Authentication.JwtBearer;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.IdentityModel.Tokens;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Security.Claims;
10 | using System.Text;
11 | using System.Threading.Tasks;
12 |
13 | namespace CleanArchitectureCosmosDB.WebAPI.Config
14 | {
15 | ///
16 | /// Authentication configuration
17 | ///
18 | public static class AuthenticationConfig
19 | {
20 | ///
21 | /// authentication configuration
22 | ///
23 | ///
24 | ///
25 | public static void SetupAuthentication(this IServiceCollection services, IConfiguration configuration)
26 | {
27 | Token token = configuration.GetSection("token").Get();
28 | byte[] secret = Encoding.ASCII.GetBytes(token.Secret);
29 |
30 | services
31 | .AddAuthentication(
32 | options =>
33 | {
34 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
35 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
36 | })
37 | .AddJwtBearer(
38 | options =>
39 | {
40 | options.RequireHttpsMetadata = true;
41 | options.SaveToken = true;
42 | options.ClaimsIssuer = token.Issuer;
43 | options.IncludeErrorDetails = true;
44 | options.Validate(JwtBearerDefaults.AuthenticationScheme);
45 | options.TokenValidationParameters =
46 | new TokenValidationParameters
47 | {
48 | ClockSkew = TimeSpan.Zero,
49 | ValidateIssuer = true,
50 | ValidateAudience = true,
51 | ValidateLifetime = true,
52 | ValidateIssuerSigningKey = true,
53 | ValidIssuer = token.Issuer,
54 | ValidAudience = token.Audience,
55 | IssuerSigningKey = new SymmetricSecurityKey(secret),
56 | NameClaimType = ClaimTypes.NameIdentifier,
57 | RequireSignedTokens = true,
58 | RequireExpirationTime = true
59 | };
60 | });
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/AuthorizationConfig.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication.JwtBearer;
2 | using Microsoft.AspNetCore.Authorization;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace CleanArchitectureCosmosDB.WebAPI.Config
6 | {
7 | ///
8 | /// Configure authorization
9 | ///
10 | public static class AuthorizationConfig
11 | {
12 | ///
13 | /// Authorization
14 | ///
15 | ///
16 | public static void SetAuthorization(this IServiceCollection services)
17 | {
18 | services.AddAuthorization(
19 | options =>
20 | {
21 | options.AddPolicy(
22 | JwtBearerDefaults.AuthenticationScheme,
23 | new AuthorizationPolicyBuilder()
24 | .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
25 | .RequireAuthenticatedUser()
26 | .Build());
27 | });
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/CachingConfig.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Interfaces;
2 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.Services;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace CleanArchitectureCosmosDB.WebAPI.Config
6 | {
7 | ///
8 | /// Setup caching
9 | ///
10 | public static class CachingConfig
11 | {
12 | ///
13 | /// In-memory Caching
14 | ///
15 | ///
16 | public static void SetupInMemoryCaching(this IServiceCollection services)
17 | {
18 | // Non-distributed in-memory cache services
19 | services.AddMemoryCache();
20 | services.AddScoped();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/DatabaseConfig.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Interfaces;
2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence;
3 | using CleanArchitectureCosmosDB.Infrastructure.AppSettings;
4 | using CleanArchitectureCosmosDB.Infrastructure.CosmosDbData.Repository;
5 | using CleanArchitectureCosmosDB.Infrastructure.Extensions;
6 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models;
7 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Services;
8 | using Microsoft.AspNetCore.Identity;
9 | using Microsoft.EntityFrameworkCore;
10 | using Microsoft.Extensions.Configuration;
11 | using Microsoft.Extensions.DependencyInjection;
12 |
13 | namespace CleanArchitectureCosmosDB.WebAPI.Config
14 | {
15 | ///
16 | /// Database related configurations
17 | ///
18 | public static class DatabaseConfig
19 | {
20 | ///
21 | /// Setup Cosmos DB
22 | ///
23 | ///
24 | ///
25 | public static void SetupCosmosDb(this IServiceCollection services, IConfiguration configuration)
26 | {
27 | // Bind database-related bindings
28 | CosmosDbSettings cosmosDbConfig = configuration.GetSection("ConnectionStrings:CleanArchitectureCosmosDB").Get();
29 | // register CosmosDB client and data repositories
30 | services.AddCosmosDb(cosmosDbConfig.EndpointUrl,
31 | cosmosDbConfig.PrimaryKey,
32 | cosmosDbConfig.DatabaseName,
33 | cosmosDbConfig.Containers);
34 |
35 | services.AddScoped();
36 | services.AddScoped();
37 | }
38 |
39 | ///
40 | /// Setup ASP.NET Core Identity DB, including connection string, Identity options, token providers, and token services, etc..
41 | ///
42 | ///
43 | ///
44 | public static void SetupIdentityDatabase(this IServiceCollection services, IConfiguration configuration)
45 | {
46 | services.AddDbContext(options =>
47 | //options.UseSqlServer(configuration.GetConnectionString("CleanArchitectureIdentity"))
48 | options.UseInMemoryDatabase("CleanArchitectureIdentity")
49 | );
50 |
51 | services.AddIdentity()
52 | .AddDefaultTokenProviders()
53 | .AddUserManager>()
54 | .AddSignInManager>()
55 | .AddEntityFrameworkStores();
56 | services.Configure(
57 | options =>
58 | {
59 | options.SignIn.RequireConfirmedEmail = true;
60 | options.User.RequireUniqueEmail = true;
61 | options.User.AllowedUserNameCharacters =
62 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
63 |
64 | // Identity : Default password settings
65 | options.Password.RequireDigit = true;
66 | options.Password.RequireLowercase = true;
67 | options.Password.RequireNonAlphanumeric = true;
68 | options.Password.RequireUppercase = true;
69 | options.Password.RequiredLength = 6;
70 | options.Password.RequiredUniqueChars = 1;
71 | });
72 |
73 | // services required using Identity
74 | services.AddScoped();
75 |
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/MediatrConfig.cs:
--------------------------------------------------------------------------------
1 | using MediatR;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using System.Reflection;
4 |
5 | namespace CleanArchitectureCosmosDB.WebAPI.Config
6 | {
7 | ///
8 | /// MediatR config
9 | ///
10 | public static class MediatrConfig
11 | {
12 | ///
13 | /// Setup mediatr to use command/query pattern and pipeline behaviors
14 | ///
15 | ///
16 | public static void SetupMediatr(this IServiceCollection services)
17 | {
18 | // MediatR, this will scan and register everything that inherits IRequest
19 | services.AddMediatR(Assembly.GetExecutingAssembly());
20 |
21 | // Register MediatR pipeline behaviors, in the same order the behaviors should be called.
22 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(Infrastructure.Behaviours.ValidationBehaviour<,>));
23 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(Infrastructure.Behaviours.UnhandledExceptionBehaviour<,>));
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/MvcConfig.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.Filters;
2 | using FluentValidation.AspNetCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using ZymLabs.NSwag.FluentValidation.AspNetCore;
5 |
6 | namespace CleanArchitectureCosmosDB.WebAPI.Config
7 | {
8 | ///
9 | /// Configure MVC options
10 | ///
11 | public static class MvcConfig
12 | {
13 | ///
14 | /// Configure controllers
15 | ///
16 | ///
17 | public static void SetupControllers(this IServiceCollection services)
18 | {
19 | // API controllers
20 | services.AddControllers(options =>
21 | // handle exceptions thrown by an action
22 | options.Filters.Add(new ApiExceptionFilterAttribute()))
23 | .AddNewtonsoftJson(options =>
24 | {
25 | // Serilize enum in string
26 | options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
27 | })
28 | .AddFluentValidation(options =>
29 | {
30 | // In order to register FluentValidation to define Swagger schema
31 | // https://github.com/RicoSuter/NSwag/issues/1722#issuecomment-544202504
32 | // https://github.com/zymlabs/nswag-fluentvalidation
33 | options.RegisterValidatorsFromAssemblyContaining();
34 |
35 | // Optionally set validator factory if you have problems with scope resolve inside validators.
36 | options.ValidatorFactoryType = typeof(HttpContextServiceProviderValidatorFactory);
37 | })
38 | .AddMvcOptions(options =>
39 | {
40 | // Clear the default MVC model binding and model validations, as we are registering all model binding and validation using FluentValidation.
41 | // See ApiExceptionFilterAttribute.cs
42 | // https://github.com/jasontaylordev/NorthwindTraders/issues/76
43 | options.ModelMetadataDetailsProviders.Clear();
44 | options.ModelValidatorProviders.Clear();
45 | });
46 | }
47 |
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/ODataConfig.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNet.OData.Extensions;
2 | using Microsoft.AspNetCore.Mvc.Formatters;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Net.Http.Headers;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 |
10 | namespace CleanArchitectureCosmosDB.WebAPI.Config
11 | {
12 | ///
13 | /// OData
14 | ///
15 | public static class ODataConfig
16 | {
17 | ///
18 | /// Setup OData
19 | ///
20 | ///
21 | public static void SetupOData(this IServiceCollection services)
22 | {
23 | // OData Support
24 | services.AddOData();
25 |
26 | // In order to make swagger work with OData
27 | services.AddMvcCore(options =>
28 | {
29 | foreach (OutputFormatter outputFormatter in options.OutputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0))
30 | {
31 | outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
32 | }
33 |
34 | foreach (InputFormatter inputFormatter in options.InputFormatters.OfType().Where(x => x.SupportedMediaTypes.Count == 0))
35 | {
36 | inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
37 | }
38 | });
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Config/SwaggerConfig.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using NSwag;
3 | using NSwag.Generation.Processors.Security;
4 | using ZymLabs.NSwag.FluentValidation;
5 |
6 | namespace CleanArchitectureCosmosDB.WebAPI.Config
7 | {
8 | ///
9 | /// Swagger
10 | ///
11 | public static class SwaggerConfig
12 | {
13 | ///
14 | /// NSwag for swagger
15 | ///
16 | ///
17 | public static void SetupNSwag(this IServiceCollection services)
18 | {
19 | // Register the Swagger services
20 | services.AddOpenApiDocument((options, serviceProvider) =>
21 | {
22 | options.DocumentName = "v1";
23 | options.Title = "Clean Architecture Cosmos DB API";
24 | options.Version = "v1";
25 |
26 | FluentValidationSchemaProcessor fluentValidationSchemaProcessor = serviceProvider.GetService();
27 | // Add the fluent validations schema processor
28 | options.SchemaProcessors.Add(fluentValidationSchemaProcessor);
29 |
30 | // Add JWT token authorization
31 | options.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth"));
32 | options.DocumentProcessors.Add(new SecurityDefinitionAppender("auth", new OpenApiSecurityScheme
33 | {
34 | Type = OpenApiSecuritySchemeType.Http,
35 | In = OpenApiSecurityApiKeyLocation.Header,
36 | Scheme = "bearer",
37 | BearerFormat = "jwt"
38 | }));
39 |
40 | });
41 |
42 | // Add the FluentValidationSchemaProcessor as a singleton
43 | services.AddSingleton();
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Controllers/AttachmentController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.WebAPI.Models.Attachment;
2 | using MediatR;
3 | using Microsoft.AspNetCore.Mvc;
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers
8 | {
9 | ///
10 | /// Controller
11 | ///
12 | [Route("api/[controller]")]
13 | [ApiController]
14 | public class AttachmentController : ControllerBase
15 | {
16 | private readonly IMediator _mediator;
17 |
18 | ///
19 | /// Controller ctor
20 | ///
21 | ///
22 | public AttachmentController(IMediator mediator)
23 | {
24 | this._mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
25 | }
26 |
27 | // GET: api/Attachment/Download?filePath=abc.JPG
28 | ///
29 | /// Download an item by path
30 | ///
31 | ///
32 | ///
33 | ///
34 | [HttpGet("Download", Name = "DownloadAttachment")]
35 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
36 | public async Task Download([FromQuery] string filePath,
37 | [FromQuery] string originalFileName = "")
38 | {
39 | var response = await _mediator.Send(
40 | new Download.DownloadQuery()
41 | {
42 | FilePath = filePath,
43 | OriginalFileName = originalFileName
44 | });
45 |
46 | return File(response.Stream, response.ContentType, response.FileName);
47 | }
48 |
49 | // POST: api/Attachment
50 | ///
51 | /// Upload an item
52 | ///
53 | ///
54 | ///
55 | [HttpPost]
56 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))]
57 | public async Task> Upload([FromForm] Models.Attachment.Upload.UploadAttachmentCommand command)
58 | {
59 | var response = await _mediator.Send(command);
60 | return CreatedAtRoute("DownloadAttachment",
61 | new
62 | {
63 | filePath = response.Resource.FilePath,
64 | originalFileName = response.Resource.OriginalFileName
65 | },
66 | response);
67 | }
68 |
69 | // POST: api/Attachment/Multiple
70 | ///
71 | /// Upload multiple files
72 | ///
73 | ///
74 | ///
75 | [HttpPost("Multiple", Name = "UploadMultipleAttachment")]
76 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)]
77 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest)]
78 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))]
79 | public async Task> UploadMultiple([FromForm] Models.Attachment.UploadMultiple.UploadMultipleAttachmentCommand command)
80 | {
81 | var response = await _mediator.Send(command);
82 |
83 | return Ok(response.UploadedAttachments);
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Controllers/ToDoItemController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Exceptions;
2 | using CleanArchitectureCosmosDB.WebAPI.Models.Shared;
3 | using CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem;
4 | using MediatR;
5 | using Microsoft.AspNet.OData;
6 | using Microsoft.AspNetCore.Authorization;
7 | using Microsoft.AspNetCore.Mvc;
8 | using System;
9 | using System.Collections.Generic;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers
13 | {
14 | ///
15 | /// ToDoItem Controller
16 | ///
17 | //[Authorize("Bearer")]
18 | [Route("api/[controller]")]
19 | [ApiController]
20 | public class ToDoItemController : ControllerBase
21 | {
22 | private readonly IMediator _mediator;
23 |
24 | ///
25 | /// Controller ctor
26 | ///
27 | ///
28 | public ToDoItemController(IMediator mediator)
29 | {
30 | this._mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
31 | }
32 |
33 | // GET: api/ToDoItem
34 | // OData: https://localhost:5001/api/ToDoItem?$select=title
35 | ///
36 | /// Get all
37 | ///
38 | ///
39 | [HttpGet]
40 | [EnableQuery]
41 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
42 | public async Task> GetAll()
43 | {
44 | GetAll.QueryResponse response = await _mediator.Send(new GetAll.GetAllQuery());
45 | return response.Resource;
46 | }
47 |
48 | // GET: api/ToDoItem/5
49 | ///
50 | /// Get by id
51 | ///
52 | ///
53 | ///
54 | [HttpGet("{id}", Name = "GetToDoItem")]
55 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
56 | public async Task> Get(string id)
57 | {
58 | Get.QueryResponse response = await _mediator.Send(new Get.GetQuery() { Id = id });
59 |
60 | return response.Resource;
61 | }
62 |
63 | // POST: api/ToDoItem
64 | ///
65 | /// Create
66 | ///
67 | ///
68 | ///
69 | [HttpPost]
70 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Create))]
71 | public async Task Create([FromBody] Create.CreateToDoItemCommand command)
72 | {
73 | Create.CommandResponse response = await _mediator.Send(command);
74 | return CreatedAtRoute("GetToDoItem", new { id = response.Id }, null);
75 | }
76 |
77 | // PUT: api/ToDoItem/5
78 | ///
79 | /// Update
80 | ///
81 | ///
82 | ///
83 | ///
84 | [HttpPut("{id}")]
85 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Update))]
86 | public async Task Update(string id, [FromBody] Update.UpdateCommand command)
87 | {
88 | if (id != command.Id)
89 | {
90 | return BadRequest();
91 | }
92 |
93 | Update.CommandResponse response = await _mediator.Send(command);
94 |
95 | return NoContent();
96 | }
97 |
98 | // DELETE: api/ToDoItem/5
99 | ///
100 | /// Delete
101 | ///
102 | ///
103 | ///
104 | [HttpDelete("{id}")]
105 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))]
106 | public async Task Delete(string id)
107 | {
108 | await _mediator.Send(new Delete.DeleteToDoItemCommand() { Id = id });
109 |
110 | return NoContent();
111 | }
112 |
113 | // GET: api/ToDoItem/5/AuditHistory
114 | ///
115 | /// Get audit history of an item by id
116 | ///
117 | ///
118 | ///
119 | [HttpGet("{id}/AuditHistory", Name = "GetToDoItemAuditHistory")]
120 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
121 | public async Task> GetAuditHistory(string id)
122 | {
123 | GetAuditHistory.QueryResponse response = await _mediator.Send(new GetAuditHistory.GetQuery() { Id = id });
124 |
125 | return response.Resource;
126 | }
127 |
128 | // Search: api/ToDoItem/Search
129 | ///
130 | /// Search
131 | ///
132 | ///
133 | ///
134 | [HttpPost("Search", Name = "SearchDefinition")]
135 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
136 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)]
137 | public async Task Search(Search.SearchToDoItemQuery query)
138 | {
139 | Search.QueryResponse response = await _mediator.Send(query);
140 | DataTablesResponse result = new DataTablesResponse()
141 | {
142 | Data = response.Resource,
143 | TotalRecords = response.TotalRecordsMatched,
144 | Page = response.CurrentPage
145 | };
146 |
147 | return result;
148 | }
149 |
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Controllers/TokenController.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
2 | using CleanArchitectureCosmosDB.WebAPI.Models.Token;
3 | using MediatR;
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.AspNetCore.Mvc;
6 | using System.Threading.Tasks;
7 |
8 | namespace CleanArchitectureCosmosDB.WebAPI.Controllers
9 | {
10 | ///
11 | /// All token related actions.
12 | ///
13 | [ApiController]
14 | [Route("api/[controller]")]
15 | public class TokenController
16 | {
17 | private readonly IMediator _mediator;
18 |
19 | ///
20 | /// ctor
21 | ///
22 | ///
23 | public TokenController(IMediator mediator)
24 | {
25 | _mediator = mediator;
26 | }
27 |
28 | // POST: api/Token/Authenticate
29 | ///
30 | /// Validate that the user account is valid and return an auth token
31 | /// to the requesting app for use in the api.
32 | ///
33 | ///
34 | ///
35 | [AllowAnonymous]
36 | [HttpPost("Authenticate")]
37 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status200OK)]
38 | [ProducesResponseType(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest)]
39 | [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
40 | public async Task AuthenticateAsync([FromBody] Authenticate.AuthenticateCommand command)
41 | {
42 | var response = await _mediator.Send(command);
43 | return response.Resource;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/ApiExceptions/ApiModelValidationException.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation.Results;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.ApiExceptions
7 | {
8 | ///
9 | /// Api validation exception
10 | ///
11 | public class ApiModelValidationException : Exception
12 | {
13 | ///
14 | /// Validation errors
15 | ///
16 | public IDictionary Errors { get; }
17 |
18 | ///
19 | /// ctor
20 | ///
21 | public ApiModelValidationException()
22 | : base("One or more validation failures have occurred.")
23 | {
24 | Errors = new Dictionary();
25 | }
26 |
27 | ///
28 | /// ctor
29 | ///
30 | ///
31 | public ApiModelValidationException(IEnumerable failures)
32 | : this()
33 | {
34 | Errors = failures
35 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
36 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
37 | }
38 |
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Behaviours/UnhandledExceptionBehaviour.cs:
--------------------------------------------------------------------------------
1 | using MediatR;
2 | using Serilog;
3 | using System;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Behaviours
8 | {
9 | ///
10 | /// MediatR pipeline behavior to handle any unhandled exception.
11 | /// For more information: https://github.com/jbogard/MediatR/wiki/Behaviors
12 | ///
13 | /// The request object passed in through IMediator.Send.
14 | ///
15 | public class UnhandledExceptionBehaviour : IPipelineBehavior
16 | {
17 | ///
18 | /// ctor
19 | ///
20 | public UnhandledExceptionBehaviour()
21 | {
22 | }
23 |
24 | ///
25 | ///
26 | ///
27 | /// The request object passed in through IMediator.Send.
28 | /// Cancellation token.
29 | /// An async continuation for the next action in the behavior chain.
30 | ///
31 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next)
32 | {
33 | try
34 | {
35 | return await next();
36 | }
37 | catch (Exception ex)
38 | {
39 | string requestName = typeof(TRequest).Name;
40 |
41 | Log.Error(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
42 |
43 | throw;
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Behaviours/ValidationBehaviour.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.WebAPI.Infrastructure.ApiExceptions;
2 | using FluentValidation;
3 | using MediatR;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Behaviours
10 | {
11 | ///
12 | /// MediatR pipeline behavior to run validation logic before the handlers handle the request.
13 | /// For more information: https://github.com/jbogard/MediatR/wiki/Behaviors
14 | ///
15 | ///
16 | ///
17 | public class ValidationBehaviour : IPipelineBehavior
18 | where TRequest : IRequest
19 | {
20 | private readonly IEnumerable> _validators;
21 |
22 | ///
23 | /// ctor
24 | ///
25 | ///
26 | public ValidationBehaviour(IEnumerable> validators)
27 | {
28 | _validators = validators;
29 | }
30 |
31 | ///
32 | /// pipeline handler
33 | ///
34 | ///
35 | ///
36 | ///
37 | ///
38 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next)
39 | {
40 | if (_validators.Any())
41 | {
42 | ValidationContext context = new ValidationContext(request);
43 |
44 | FluentValidation.Results.ValidationResult[] validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
45 | List failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();
46 |
47 | if (failures.Count != 0)
48 | throw new ApiModelValidationException(failures);
49 | }
50 | return await next();
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Infrastructure/Services/InMemoryCachedToDoItemsService.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Infrastructure.Extensions;
4 | using Microsoft.Extensions.Caching.Memory;
5 | using System;
6 | using System.Collections.Generic;
7 |
8 | namespace CleanArchitectureCosmosDB.WebAPI.Infrastructure.Services
9 | {
10 | ///
11 | /// Non-distributed in memory cache.
12 | ///
13 | public class InMemoryCachedToDoItemsService : ICachedToDoItemsService
14 | {
15 | private readonly IMemoryCache _cache;
16 |
17 | ///
18 | /// ctor
19 | ///
20 | ///
21 | public InMemoryCachedToDoItemsService(IMemoryCache cache)
22 | {
23 | _cache = cache ?? throw new ArgumentNullException(nameof(cache));
24 | }
25 |
26 | ///
27 | /// Delete
28 | ///
29 | ///
30 | public void DeleteCachedToDoItems()
31 | {
32 | _cache.Remove(CacheHelpers.GenerateToDoItemsCacheKey());
33 | }
34 |
35 | ///
36 | /// Get
37 | ///
38 | ///
39 | public IEnumerable GetCachedToDoItems()
40 | {
41 | IEnumerable toDoItems;
42 |
43 | _cache.TryGetValue>(CacheHelpers.GenerateToDoItemsCacheKey(), out toDoItems);
44 |
45 | return toDoItems;
46 | }
47 |
48 | ///
49 | /// Set
50 | ///
51 | ///
52 | public void SetCachedToDoItems(IEnumerable entry)
53 | {
54 | // Set cache options
55 | MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
56 | // Keep in cache for this time, reset time if accessed.
57 | .SetSlidingExpiration(TimeSpan.FromDays(1));
58 |
59 | _cache.Set(CacheHelpers.GenerateToDoItemsCacheKey(), entry, cacheEntryOptions);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/AttachmentModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 |
7 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment
8 | {
9 | ///
10 | /// Attachment
11 | ///
12 | public class AttachmentModel
13 | {
14 | public AttachmentModel()
15 | {
16 | this.Id = Guid.NewGuid();
17 | }
18 |
19 | [Required]
20 | public Guid Id { get; set; }
21 |
22 | public string FileName { get; set; }
23 | public string FileType { get; set; }
24 | public string FilePath { get; set; }
25 | public string OriginalFileName { get; set; }
26 | public string Name { get; set; }
27 | public string Description { get; set; }
28 | public string ContentType { get; set; }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/Download.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Exceptions;
3 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.IO;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment
12 | {
13 |
14 | ///
15 | /// Download related query, validators, and handlers
16 | ///
17 | public class Download
18 | {
19 | ///
20 | /// Model to Download
21 | ///
22 | public class DownloadQuery : IRequest
23 | {
24 | ///
25 | /// Full Path
26 | ///
27 | public string FilePath { get; set; }
28 |
29 | ///
30 | /// Original name of the file
31 | ///
32 | public string OriginalFileName { get; set; }
33 | }
34 |
35 | ///
36 | /// Query Response
37 | ///
38 | public class QueryResponse
39 | {
40 | ///
41 | /// File Name
42 | ///
43 | public string FileName { get; set; }
44 |
45 | ///
46 | /// Content Type
47 | ///
48 | public string ContentType { get; set; }
49 |
50 | ///
51 | /// Stream
52 | ///
53 | public Stream Stream { get; set; }
54 | }
55 |
56 | ///
57 | /// Register Validation
58 | ///
59 | public class DownloadAttachmentQueryValidator : AbstractValidator
60 | {
61 | ///
62 | /// Validator ctor
63 | ///
64 | public DownloadAttachmentQueryValidator()
65 | {
66 | RuleFor(x => x.FilePath)
67 | .NotEmpty();
68 | }
69 |
70 | }
71 |
72 |
73 | ///
74 | /// Handler
75 | ///
76 | public class QueryHandler : IRequestHandler
77 | {
78 | private readonly IStorageService _storageService;
79 | private readonly IMapper _mapper;
80 |
81 | ///
82 | /// Ctor
83 | ///
84 | ///
85 | ///
86 | public QueryHandler(IStorageService storageService,
87 | IMapper mapper)
88 | {
89 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
90 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
91 | }
92 |
93 | ///
94 | /// Handle
95 | ///
96 | ///
97 | ///
98 | ///
99 | public async Task Handle(DownloadQuery query, CancellationToken cancellationToken)
100 | {
101 | QueryResponse response = new QueryResponse();
102 | Stream ms = new MemoryStream();
103 |
104 | try
105 | {
106 | ms = await _storageService.GetFileStream(query.FilePath);
107 | ms.Position = 0; // Have to reset the current position
108 | }
109 | catch(Exception ex)
110 | {
111 | // Exception and logging are handled in a centralized place, see ApiExceptionFilter.
112 | throw new EntityNotFoundException(nameof(AttachmentModel), query.FilePath);
113 | }
114 |
115 | // Content Type mapping
116 | string[] fileNameParts = query.FilePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
117 | string fileName = fileNameParts[fileNameParts.Length - 1];
118 |
119 | var contentTypeProvider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
120 | string contentType = "application/octet-stream";
121 | if (!contentTypeProvider.TryGetContentType(fileName, out contentType))
122 | {
123 | contentType = "application/octet-stream"; // fallback
124 | }
125 |
126 | response.FileName = String.IsNullOrEmpty(query.OriginalFileName) ? fileName : query.OriginalFileName;
127 | response.Stream = ms;
128 | response.ContentType = contentType;
129 |
130 | return response;
131 | }
132 |
133 | }
134 |
135 |
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/Upload.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage;
3 | using FluentValidation;
4 | using MediatR;
5 | using Microsoft.AspNetCore.Http;
6 | using Storage.Net;
7 | using System;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment
12 | {
13 | ///
14 | /// Upload related commands, validators and handlers
15 | ///
16 | public class Upload
17 | {
18 | ///
19 | /// Model to create a new Attachment
20 | ///
21 | public class UploadAttachmentCommand : IRequest
22 | {
23 |
24 | ///
25 | /// The binary file to upload
26 | ///
27 | public IFormFile File { get; set; }
28 |
29 | }
30 |
31 | ///
32 | /// Command Response
33 | ///
34 | public class CommandResponse
35 | {
36 | ///
37 | /// Attachment that is uploaded
38 | ///
39 | public AttachmentModel Resource { get; set; }
40 | }
41 |
42 | ///
43 | /// Register Validation
44 | ///
45 | public class UploadAttachmentCommandValidator : AbstractValidator
46 | {
47 | private readonly IStorageService _storageService;
48 |
49 | ///
50 | /// Validator ctor
51 | ///
52 | public UploadAttachmentCommandValidator(IStorageService storageService)
53 | {
54 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
55 |
56 | // Add Validation rules here
57 |
58 | }
59 |
60 |
61 | }
62 |
63 |
64 | ///
65 | /// Handler
66 | ///
67 | public class CommandHandler : IRequestHandler
68 | {
69 | private readonly IStorageService _storageService;
70 | private readonly IMapper _mapper;
71 |
72 | ///
73 | /// Ctor
74 | ///
75 | ///
76 | ///
77 | public CommandHandler(IStorageService storageService,
78 | IMapper mapper)
79 | {
80 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
81 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
82 | }
83 |
84 | ///
85 | /// Handle
86 | ///
87 | ///
88 | ///
89 | ///
90 | public async Task Handle(UploadAttachmentCommand command, CancellationToken cancellationToken)
91 | {
92 | CommandResponse response = new CommandResponse();
93 | AttachmentModel uploadedAttachment = new AttachmentModel();
94 | // prepare the upload
95 | string originalName = System.IO.Path.GetFileName(command.File.FileName);
96 | string extension = System.IO.Path.GetExtension(command.File.FileName).ToLower();
97 | string storedAsFileName = $"{uploadedAttachment.Id}{extension}";
98 | string fileFullPath = StoragePath.Combine("attachments",
99 | storedAsFileName);
100 |
101 | // upload
102 | string fullPath = await _storageService.UploadFile(command.File, fileFullPath);
103 | uploadedAttachment.FilePath = fullPath;
104 |
105 | // prepare response
106 | uploadedAttachment.FileName = storedAsFileName;
107 | uploadedAttachment.FileType = extension;
108 | uploadedAttachment.ContentType = command.File.ContentType;
109 | uploadedAttachment.OriginalFileName = originalName;
110 | uploadedAttachment.Name = originalName;
111 | uploadedAttachment.Description = $"Attachment {originalName}";
112 |
113 | response.Resource = uploadedAttachment;
114 |
115 | return response;
116 | }
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Attachment/UploadMultiple.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces.Storage;
3 | using FluentValidation;
4 | using MediatR;
5 | using Microsoft.AspNetCore.Http;
6 | using Storage.Net;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Attachment
13 | {
14 | ///
15 | /// UploadMultiple related commands, validators and handlers
16 | ///
17 | public class UploadMultiple
18 | {
19 | ///
20 | /// Model to create a new Attachment
21 | ///
22 | public class UploadMultipleAttachmentCommand : IRequest
23 | {
24 |
25 | ///
26 | /// The binary file to upload
27 | ///
28 | public IEnumerable Files { get; set; }
29 |
30 | }
31 |
32 | ///
33 | /// Command Response
34 | ///
35 | public class CommandResponse
36 | {
37 | ///
38 | /// Attachments uploaded
39 | ///
40 | public List UploadedAttachments { get; set; }
41 | }
42 |
43 | ///
44 | /// Register Validation
45 | ///
46 | public class UploadMultipleAttachmentCommandValidator : AbstractValidator
47 | {
48 | private readonly IStorageService _storageService;
49 |
50 | ///
51 | /// Validator ctor
52 | ///
53 | public UploadMultipleAttachmentCommandValidator(IStorageService storageService)
54 | {
55 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
56 |
57 | // Add Validation rules here
58 |
59 | }
60 |
61 |
62 | }
63 |
64 |
65 | ///
66 | /// Handler
67 | ///
68 | public class CommandHandler : IRequestHandler
69 | {
70 | private readonly IStorageService _storageService;
71 | private readonly IMapper _mapper;
72 |
73 | ///
74 | /// Ctor
75 | ///
76 | ///
77 | ///
78 | public CommandHandler(IStorageService storageService,
79 | IMapper mapper)
80 | {
81 | this._storageService = storageService ?? throw new ArgumentNullException(nameof(storageService));
82 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
83 | }
84 |
85 | ///
86 | /// Handle
87 | ///
88 | ///
89 | ///
90 | ///
91 | public async Task Handle(UploadMultipleAttachmentCommand command, CancellationToken cancellationToken)
92 | {
93 | CommandResponse response = new CommandResponse()
94 | {
95 | UploadedAttachments = new List()
96 | };
97 |
98 | foreach(var commandFile in command.Files)
99 | {
100 | AttachmentModel attachment = new
101 | AttachmentModel();
102 |
103 | // prepare the upload
104 | string originalName = System.IO.Path.GetFileName(commandFile.FileName);
105 | string extension = System.IO.Path.GetExtension(commandFile.FileName).ToLower();
106 | string storedAsFileName = $"{attachment.Id}{extension}";
107 | string fileFullPath = StoragePath.Combine("attachments",
108 | storedAsFileName);
109 |
110 | // upload
111 | string fullPath = await _storageService.UploadFile(commandFile, fileFullPath);
112 | attachment.FilePath = fullPath;
113 |
114 | // prepare response
115 | attachment.FileName = storedAsFileName;
116 | attachment.FileType = extension;
117 | attachment.ContentType = commandFile.ContentType;
118 | attachment.OriginalFileName = originalName;
119 | attachment.Name = originalName;
120 | attachment.Description = $"Attachment {originalName}";
121 |
122 | response.UploadedAttachments.Add(attachment);
123 | }
124 |
125 | return response;
126 | }
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Shared/DataTablesResponse.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Shared
4 | {
5 | public class DataTablesResponse
6 | {
7 | ///
8 | /// Total number of records available
9 | ///
10 | [Required]
11 | public int TotalRecords { get; set; }
12 | ///
13 | /// Data object
14 | ///
15 | [Required]
16 | public object Data { get; set; }
17 |
18 | ///
19 | /// Current page index
20 | ///
21 | [Required]
22 | public int Page { get; set; }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Create.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Core.Specifications;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.Linq;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
12 | {
13 | ///
14 | /// Create related commands, validators and handlers
15 | ///
16 | public class Create
17 | {
18 | ///
19 | /// Model to create an entity
20 | ///
21 | public class CreateToDoItemCommand : IRequest
22 | {
23 | ///
24 | /// Category
25 | ///
26 | public string Category { get; set; }
27 |
28 | ///
29 | /// Title
30 | ///
31 | public string Title { get; set; }
32 |
33 |
34 | }
35 |
36 | ///
37 | /// Command Response
38 | ///
39 | public class CommandResponse
40 | {
41 | ///
42 | /// Item Id
43 | ///
44 | public string Id { get; set; }
45 | }
46 |
47 | ///
48 | /// Register Validation
49 | ///
50 | public class CreateToDoItemCommandValidator : AbstractValidator
51 | {
52 | private readonly IToDoItemRepository _repo;
53 |
54 | ///
55 | /// Validator ctor
56 | ///
57 | public CreateToDoItemCommandValidator(IToDoItemRepository repo)
58 | {
59 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
60 |
61 | RuleFor(x => x.Category)
62 | .NotEmpty();
63 | RuleFor(x => x.Title)
64 | .Cascade(CascadeMode.Stop)
65 | .NotEmpty()
66 | .MustAsync(HasUniqueTitle).WithMessage("Title must be unique");
67 |
68 | }
69 |
70 | ///
71 | /// Check uniqueness
72 | ///
73 | ///
74 | ///
75 | ///
76 | public async Task HasUniqueTitle(string title, CancellationToken cancellationToken)
77 | {
78 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(title,
79 | exactSearch: true);
80 |
81 | System.Collections.Generic.IEnumerable entities = await _repo.GetItemsAsync(specification);
82 |
83 | return entities == null || entities.Count() == 0;
84 |
85 | }
86 | }
87 |
88 |
89 | ///
90 | /// Handler
91 | ///
92 | public class CommandHandler : IRequestHandler
93 | {
94 | private readonly IToDoItemRepository _repo;
95 | private readonly IMapper _mapper;
96 |
97 | ///
98 | /// Ctor
99 | ///
100 | ///
101 | ///
102 | public CommandHandler(IToDoItemRepository repo,
103 | IMapper mapper)
104 | {
105 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
106 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
107 | }
108 |
109 | ///
110 | /// Handle
111 | ///
112 | ///
113 | ///
114 | ///
115 | public async Task Handle(CreateToDoItemCommand command, CancellationToken cancellationToken)
116 | {
117 | CommandResponse response = new CommandResponse();
118 | Core.Entities.ToDoItem entity = _mapper.Map(command);
119 | await _repo.AddItemAsync(entity);
120 |
121 | response.Id = entity.Id;
122 | return response;
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Delete.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Exceptions;
3 | using CleanArchitectureCosmosDB.Core.Interfaces;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
11 | {
12 | ///
13 | /// Delete related commands, validators, and handlers
14 | ///
15 | public class Delete
16 | {
17 | ///
18 | /// Model to Delete an entity
19 | ///
20 | public class DeleteToDoItemCommand : IRequest
21 | {
22 | ///
23 | /// Id
24 | ///
25 | public string Id { get; set; }
26 |
27 | }
28 |
29 | ///
30 | /// Command Response
31 | ///
32 | public class CommandResponse
33 | {
34 | }
35 |
36 | ///
37 | /// Register Validation
38 | ///
39 | public class DeleteToDoItemCommandValidator : AbstractValidator
40 | {
41 | ///
42 | /// Validator ctor
43 | ///
44 | public DeleteToDoItemCommandValidator()
45 | {
46 | RuleFor(x => x.Id)
47 | .NotEmpty();
48 | }
49 |
50 | }
51 |
52 |
53 | ///
54 | /// Handler
55 | ///
56 | public class CommandHandler : IRequestHandler
57 | {
58 | private readonly IToDoItemRepository _repo;
59 | private readonly IMapper _mapper;
60 |
61 | ///
62 | /// Ctor
63 | ///
64 | ///
65 | ///
66 | public CommandHandler(IToDoItemRepository repo,
67 | IMapper mapper)
68 | {
69 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
70 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
71 | }
72 |
73 | ///
74 | /// Handle
75 | ///
76 | ///
77 | ///
78 | ///
79 | public async Task Handle(DeleteToDoItemCommand command, CancellationToken cancellationToken)
80 | {
81 | CommandResponse response = new CommandResponse();
82 |
83 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(command.Id);
84 | if (entity == null)
85 | {
86 | throw new EntityNotFoundException(nameof(ToDoItem), command.Id);
87 | }
88 |
89 | await _repo.DeleteItemAsync(command.Id);
90 |
91 | return response;
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Get.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Exceptions;
3 | using CleanArchitectureCosmosDB.Core.Interfaces;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
11 | {
12 | ///
13 | /// Get related query, validators, and handlers
14 | ///
15 | public class Get
16 | {
17 | ///
18 | /// Model to Get an entity
19 | ///
20 | public class GetQuery : IRequest
21 | {
22 | ///
23 | /// Id
24 | ///
25 | public string Id { get; set; }
26 |
27 | }
28 |
29 | ///
30 | /// Query Response
31 | ///
32 | public class QueryResponse
33 | {
34 | ///
35 | /// Resource
36 | ///
37 | public ToDoItemModel Resource { get; set; }
38 | }
39 |
40 | ///
41 | /// Register Validation
42 | ///
43 | public class GetToDoItemQueryValidator : AbstractValidator
44 | {
45 | ///
46 | /// Validator ctor
47 | ///
48 | public GetToDoItemQueryValidator()
49 | {
50 | RuleFor(x => x.Id)
51 | .NotEmpty();
52 | }
53 |
54 | }
55 |
56 |
57 | ///
58 | /// Handler
59 | ///
60 | public class QueryHandler : IRequestHandler
61 | {
62 | private readonly IToDoItemRepository _repo;
63 | private readonly IMapper _mapper;
64 |
65 | ///
66 | /// Ctor
67 | ///
68 | ///
69 | ///
70 | public QueryHandler(IToDoItemRepository repo,
71 | IMapper mapper)
72 | {
73 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
74 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
75 | }
76 |
77 | ///
78 | /// Handle
79 | ///
80 | ///
81 | ///
82 | ///
83 | public async Task Handle(GetQuery query, CancellationToken cancellationToken)
84 | {
85 | QueryResponse response = new QueryResponse();
86 |
87 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(query.Id);
88 | if (entity == null)
89 | {
90 | throw new EntityNotFoundException(nameof(ToDoItem), query.Id);
91 | }
92 |
93 | response.Resource = _mapper.Map(entity);
94 |
95 | return response;
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/GetAll.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Core.Specifications;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
13 | {
14 | ///
15 | /// GetAll related commands, validators, and handlers
16 | ///
17 | public class GetAll
18 | {
19 | ///
20 | /// Model to GetAll entities
21 | ///
22 | public class GetAllQuery : IRequest
23 | {
24 |
25 | }
26 |
27 | ///
28 | /// Query Response
29 | ///
30 | public class QueryResponse
31 | {
32 | ///
33 | /// Resource
34 | ///
35 | public IEnumerable Resource { get; set; }
36 | }
37 |
38 | ///
39 | /// Register Validation
40 | ///
41 | public class GetAllToDoItemQueryValidator : AbstractValidator
42 | {
43 | ///
44 | /// Validator ctor
45 | ///
46 | public GetAllToDoItemQueryValidator()
47 | {
48 |
49 | }
50 |
51 | }
52 |
53 | ///
54 | /// Handler
55 | ///
56 | public class QueryHandler : IRequestHandler
57 | {
58 | private readonly IToDoItemRepository _repo;
59 | private readonly IMapper _mapper;
60 | private readonly ICachedToDoItemsService _cachedToDoItemsService;
61 |
62 | ///
63 | /// Ctor
64 | ///
65 | ///
66 | ///
67 | ///
68 | public QueryHandler(IToDoItemRepository repo,
69 | IMapper mapper,
70 | ICachedToDoItemsService cachedToDoItemsService)
71 | {
72 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
73 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
74 | this._cachedToDoItemsService = cachedToDoItemsService ?? throw new ArgumentNullException(nameof(cachedToDoItemsService));
75 | }
76 |
77 | ///
78 | /// Handle
79 | ///
80 | ///
81 | ///
82 | ///
83 | public async Task Handle(GetAllQuery query, CancellationToken cancellationToken)
84 | {
85 | QueryResponse response = new QueryResponse();
86 |
87 | // If needed, this is where to implement cache reading and setting logic
88 | //var cachedEntities = await _cachedToDoItemsService.GetCachedToDoItemsAsync();
89 |
90 | //var entities = await _repo.GetItemsAsync($"SELECT * FROM c");
91 | // Get all the incompleted todo items
92 | ToDoItemGetAllSpecification specification = new ToDoItemGetAllSpecification(false);
93 | IEnumerable entities = await _repo.GetItemsAsync(specification);
94 | response.Resource = entities.Select(x => _mapper.Map(x));
95 |
96 | return response;
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/GetAuditHistory.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces.Persistence;
3 | using CleanArchitectureCosmosDB.Core.Specifications;
4 | using FluentValidation;
5 | using MediatR;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
13 | {
14 | ///
15 | /// Get related query, validators, and handlers
16 | ///
17 | public class GetAuditHistory
18 | {
19 | ///
20 | /// Get
21 | ///
22 | public class GetQuery : IRequest
23 | {
24 | ///
25 | /// Entity Id
26 | ///
27 | public string Id { get; set; }
28 |
29 | }
30 |
31 | ///
32 | /// Query Response
33 | ///
34 | public class QueryResponse
35 | {
36 | ///
37 | /// Resource
38 | ///
39 | public IEnumerable Resource { get; set; }
40 | }
41 |
42 | ///
43 | /// Register Validation
44 | ///
45 | public class GetDefinitionQueryValidator : AbstractValidator
46 | {
47 | ///
48 | /// Validator ctor
49 | ///
50 | public GetDefinitionQueryValidator()
51 | {
52 | RuleFor(x => x.Id)
53 | .NotEmpty();
54 | }
55 |
56 | }
57 |
58 |
59 | ///
60 | /// Handler
61 | ///
62 | public class QueryHandler : IRequestHandler
63 | {
64 | private readonly IAuditRepository _repo;
65 | private readonly IMapper _mapper;
66 |
67 | ///
68 | /// Ctor
69 | ///
70 | ///
71 | ///
72 | public QueryHandler(IAuditRepository repo,
73 | IMapper mapper)
74 | {
75 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
76 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
77 | }
78 |
79 | ///
80 | /// Handle
81 | ///
82 | ///
83 | ///
84 | ///
85 | public async Task Handle(GetQuery query, CancellationToken cancellationToken)
86 | {
87 | QueryResponse response = new QueryResponse();
88 |
89 | AuditFilterSpecification specification = new AuditFilterSpecification(query.Id);
90 | IEnumerable entities = await _repo.GetItemsAsync(specification);
91 |
92 | // Map audit records to entity-specific audit model
93 | response.Resource = entities.Select(x => _mapper.Map(x));
94 |
95 | return response;
96 | }
97 |
98 | }
99 |
100 |
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/MappingProfile.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 |
3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
4 | {
5 | ///
6 | /// Mapping Profile for AutoMapper
7 | ///
8 | public class MappingProfile : Profile
9 | {
10 | ///
11 | /// ctor
12 | ///
13 | public MappingProfile()
14 | {
15 | // Get
16 | CreateMap().ReverseMap();
17 |
18 | // Create
19 | CreateMap();
20 |
21 | // Audit
22 | CreateMap()
23 | .ForMember(t => t.ToDoItemModel, s => s.MapFrom(audit => Newtonsoft.Json.JsonConvert.DeserializeObject(audit.Entity)));
24 | }
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Search.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Interfaces;
3 | using CleanArchitectureCosmosDB.Core.Specifications;
4 | using CleanArchitectureCosmosDB.Core.Specifications.Interfaces;
5 | using FluentValidation;
6 | using MediatR;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
14 | {
15 | ///
16 | /// Search related commands, validators, and handlers
17 | ///
18 | public class Search
19 | {
20 | ///
21 | /// Model to Search
22 | ///
23 | public class SearchToDoItemQuery : IRequest, ISearchQuery
24 | {
25 | // Pagination and Sort
26 | ///
27 | /// Starting point (translates to OFFSET)
28 | ///
29 | public int Start { get; set; }
30 | ///
31 | /// Page Size (translates to LIMIT)
32 | ///
33 | public int PageSize { get; set; }
34 | ///
35 | /// Sort by Column
36 | ///
37 | public string SortColumn { get; set; }
38 | ///
39 | /// Sort direction
40 | ///
41 | public SortDirection? SortDirection { get; set; }
42 |
43 | // Search
44 | ///
45 | /// Title
46 | ///
47 | public string TitleFilter { get; set; }
48 | }
49 |
50 | ///
51 | /// Query Response
52 | ///
53 | public class QueryResponse
54 | {
55 | ///
56 | /// Current Page, 0-indexed
57 | ///
58 | public int CurrentPage { get; set; }
59 |
60 | ///
61 | /// Total Records Matched. For Pagination purpose.
62 | ///
63 | public int TotalRecordsMatched { get; set; }
64 |
65 | ///
66 | /// Resource
67 | ///
68 | public IEnumerable Resource { get; set; }
69 | }
70 |
71 |
72 |
73 | ///
74 | /// Register Validation
75 | ///
76 | public class SearchToDoItemQueryValidator : AbstractValidator
77 | {
78 | ///
79 | /// Validator ctor
80 | ///
81 | public SearchToDoItemQueryValidator()
82 | {
83 | RuleFor(x => x.PageSize)
84 | .NotEmpty()
85 | .GreaterThan(0);
86 |
87 | }
88 |
89 | }
90 |
91 | ///
92 | /// Handler
93 | ///
94 | public class QueryHandler : IRequestHandler
95 | {
96 | private readonly IToDoItemRepository _repo;
97 | private readonly IMapper _mapper;
98 |
99 | ///
100 | /// Ctor
101 | ///
102 | ///
103 | ///
104 | public QueryHandler(IToDoItemRepository repo,
105 | IMapper mapper)
106 | {
107 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
108 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
109 | }
110 |
111 | ///
112 | /// Handle
113 | ///
114 | ///
115 | ///
116 | ///
117 | public async Task Handle(SearchToDoItemQuery query, CancellationToken cancellationToken)
118 | {
119 | QueryResponse response = new QueryResponse();
120 |
121 | // records
122 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(query.TitleFilter,
123 | query.Start,
124 | query.PageSize,
125 | query.SortColumn,
126 | query.SortDirection ?? query.SortDirection.Value);
127 |
128 | IEnumerable entities = await _repo.GetItemsAsync(specification);
129 | response.Resource = entities.Select(x => _mapper.Map(x));
130 |
131 | // count
132 | ToDoItemSearchAggregationSpecification countSpecification = new ToDoItemSearchAggregationSpecification(query.TitleFilter);
133 | response.TotalRecordsMatched = await _repo.GetItemsCountAsync(countSpecification);
134 |
135 | response.CurrentPage = (query.PageSize != 0) ? query.Start / query.PageSize : 0;
136 |
137 | return response;
138 | }
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/ToDoItemAuditModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel.DataAnnotations;
3 |
4 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
5 | {
6 | ///
7 | /// ToDoItem audit Model
8 | ///
9 | public class ToDoItemAuditModel
10 | {
11 | ///
12 | /// Snapshot of the ToDoItem
13 | ///
14 | [Required]
15 | public ToDoItemModel ToDoItemModel { get; set; }
16 | ///
17 | /// Date audit record created
18 | ///
19 | [Required]
20 | public DateTime DateCreatedUTC { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/ToDoItemModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
4 | {
5 | ///
6 | /// ToDoItem Api Model
7 | ///
8 | public class ToDoItemModel
9 | {
10 | ///
11 | /// ToDoItem Id
12 | ///
13 | [Required]
14 | public string Id { get; set; }
15 | ///
16 | /// Category which the To-Do-Item belongs to
17 | ///
18 | [Required]
19 | public string Category { get; set; }
20 | ///
21 | /// Title of the To-Do-Item
22 | ///
23 | [Required]
24 | public string Title { get; set; }
25 |
26 | ///
27 | /// Whether the To-Do-Item is done
28 | ///
29 | [Required]
30 | public bool IsCompleted { get; private set; }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/ToDoItem/Update.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Exceptions;
3 | using CleanArchitectureCosmosDB.Core.Interfaces;
4 | using CleanArchitectureCosmosDB.Core.Specifications;
5 | using FluentValidation;
6 | using MediatR;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Linq;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 |
14 | namespace CleanArchitectureCosmosDB.WebAPI.Models.ToDoItem
15 | {
16 | ///
17 | /// Update related commands, validators, and handlers
18 | ///
19 | public class Update
20 | {
21 | ///
22 | /// Model to Update an entity
23 | ///
24 | public class UpdateCommand : IRequest
25 | {
26 | ///
27 | /// Id
28 | ///
29 | public string Id { get; set; }
30 |
31 | ///
32 | /// Category
33 | ///
34 | public string Category { get; set; }
35 |
36 | ///
37 | /// Title
38 | ///
39 | public string Title { get; set; }
40 |
41 | }
42 |
43 | ///
44 | /// Command Response
45 | ///
46 | public class CommandResponse
47 | {
48 |
49 | }
50 |
51 | ///
52 | /// Register Validation
53 | ///
54 | public class UpdateToDoItemCommandValidator : AbstractValidator
55 | {
56 | private readonly IToDoItemRepository _repo;
57 |
58 | ///
59 | /// Validator ctor
60 | ///
61 | public UpdateToDoItemCommandValidator(IToDoItemRepository repo)
62 | {
63 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
64 |
65 | RuleFor(x => x.Id)
66 | .NotEmpty();
67 |
68 | RuleFor(x => x.Category)
69 | .NotEmpty();
70 |
71 | RuleFor(x => x.Title)
72 | .NotEmpty();
73 | }
74 |
75 | ///
76 | /// Check uniqueness
77 | ///
78 | ///
79 | ///
80 | ///
81 | ///
82 | public async Task HasUniqueName(UpdateCommand command, string title, CancellationToken cancellationToken)
83 | {
84 | ToDoItemSearchSpecification specification = new ToDoItemSearchSpecification(title,
85 | exactSearch: true);
86 |
87 | IEnumerable entities = await _repo.GetItemsAsync(specification);
88 |
89 | return entities == null ||
90 | entities.Count() == 0 ||
91 | // self
92 | entities.All(x => x.Id == command.Id);
93 |
94 | }
95 | }
96 |
97 |
98 | ///
99 | /// Handler
100 | ///
101 | public class CommandHandler : IRequestHandler
102 | {
103 | private readonly IToDoItemRepository _repo;
104 | private readonly IMapper _mapper;
105 |
106 | ///
107 | /// Ctor
108 | ///
109 | ///
110 | ///
111 | public CommandHandler(IToDoItemRepository repo,
112 | IMapper mapper)
113 | {
114 | this._repo = repo ?? throw new ArgumentNullException(nameof(repo));
115 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
116 | }
117 |
118 | ///
119 | /// Handle
120 | ///
121 | ///
122 | ///
123 | ///
124 | public async Task Handle(UpdateCommand command, CancellationToken cancellationToken)
125 | {
126 | CommandResponse response = new CommandResponse();
127 |
128 | Core.Entities.ToDoItem entity = await _repo.GetItemAsync(command.Id);
129 | if (entity == null)
130 | {
131 | throw new EntityNotFoundException(nameof(ToDoItem), command.Id);
132 | }
133 |
134 | entity.Category = command.Category;
135 | entity.Title = command.Title;
136 | await _repo.UpdateItemAsync(command.Id, entity);
137 |
138 | return response;
139 | }
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Models/Token/Authenticate.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Core.Exceptions;
3 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Services;
5 | using FluentValidation;
6 | using MediatR;
7 | using Microsoft.AspNetCore.Http;
8 | using System;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace CleanArchitectureCosmosDB.WebAPI.Models.Token
13 | {
14 | ///
15 | /// Authenticate
16 | ///
17 | public class Authenticate
18 | {
19 | ///
20 | /// command
21 | ///
22 | public class AuthenticateCommand : TokenRequest, IRequest
23 | {
24 | }
25 |
26 | ///
27 | /// Response
28 | ///
29 | public class CommandResponse
30 | {
31 | ///
32 | /// Resource
33 | ///
34 | public TokenResponse Resource { get; set; }
35 | }
36 |
37 | ///
38 | /// Register Validation
39 | ///
40 | public class AuthenticateCommandValidator : AbstractValidator
41 | {
42 |
43 | ///
44 | /// Validator ctor
45 | ///
46 | public AuthenticateCommandValidator()
47 | {
48 | RuleFor(x => x.Username)
49 | .Cascade(CascadeMode.Stop)
50 | .NotEmpty();
51 |
52 | RuleFor(x => x.Password)
53 | .Cascade(CascadeMode.Stop)
54 | .NotEmpty();
55 | }
56 |
57 | }
58 |
59 | ///
60 | /// Handler
61 | ///
62 | public class CommandHandler : IRequestHandler
63 | {
64 | private readonly ITokenService _tokenService;
65 | private readonly IMapper _mapper;
66 | private readonly HttpContext _httpContext;
67 |
68 | ///
69 | /// ctor
70 | ///
71 | ///
72 | ///
73 | ///
74 | public CommandHandler(ITokenService tokenService,
75 | IMapper mapper,
76 | IHttpContextAccessor httpContextAccessor)
77 | {
78 | this._tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
79 | this._mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
80 | this._httpContext = (httpContextAccessor != null) ? httpContextAccessor.HttpContext : throw new ArgumentNullException(nameof(httpContextAccessor));
81 |
82 | }
83 |
84 | ///
85 | /// Handle
86 | ///
87 | ///
88 | ///
89 | ///
90 | public async Task Handle(AuthenticateCommand command, CancellationToken cancellationToken)
91 | {
92 | CommandResponse response = new CommandResponse();
93 |
94 | string ipAddress = _httpContext.Connection.RemoteIpAddress.MapToIPv4().ToString();
95 |
96 | TokenResponse tokenResponse = await _tokenService.Authenticate(command, ipAddress);
97 | if (tokenResponse == null)
98 | {
99 | throw new InvalidCredentialsException();
100 | }
101 |
102 | response.Resource = tokenResponse;
103 | return response;
104 | }
105 | }
106 |
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.Hosting;
4 | using Serilog;
5 | using System;
6 | using System.IO;
7 |
8 | namespace CleanArchitectureCosmosDB.WebAPI
9 | {
10 | ///
11 | /// Program
12 | ///
13 | public class Program
14 | {
15 | ///
16 | /// Configuration
17 | ///
18 | public static IConfiguration Configuration { get; private set; }
19 |
20 | ///
21 | /// Main entry point
22 | ///
23 | ///
24 | public static void Main(string[] args)
25 | {
26 | Configuration = new ConfigurationBuilder()
27 | .SetBasePath(Directory.GetCurrentDirectory())
28 | .AddJsonFile("appsettings.json", false, true)
29 | .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true,
30 | true)
31 | .AddCommandLine(args)
32 | .AddEnvironmentVariables()
33 | .Build();
34 |
35 | // configure serilog
36 | Log.Logger = new LoggerConfiguration()
37 | .ReadFrom.Configuration(Configuration)
38 | .Enrich.FromLogContext()
39 | .Enrich.WithMachineName()
40 | .CreateLogger();
41 |
42 |
43 | try
44 | {
45 | Log.Information("Starting up...");
46 | CreateHostBuilder(args).Build().Run();
47 | Log.Information("Shutting down...");
48 | }
49 | catch (Exception ex)
50 | {
51 | Log.Fatal(ex, "Host terminated unexpectedly");
52 | }
53 | finally
54 | {
55 | Log.CloseAndFlush();
56 | }
57 | }
58 |
59 | ///
60 | /// CreateHostBuilder
61 | ///
62 | ///
63 | ///
64 | public static IHostBuilder CreateHostBuilder(string[] args) =>
65 | Host.CreateDefaultBuilder(args)
66 | .ConfigureWebHostDefaults(webBuilder =>
67 | {
68 | webBuilder.UseStartup()
69 | .UseConfiguration(Configuration)
70 | .UseSerilog(); ;
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:52349",
7 | "sslPort": 44315
8 | }
9 | },
10 | "$schema": "http://json.schemastore.org/launchsettings.json",
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "weatherforecast",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "CleanArchitectureCosmosDB.WebAPI": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "swagger",
24 | "environmentVariables": {
25 | "ASPNETCORE_ENVIRONMENT": "Development"
26 | },
27 | "applicationUrl": "https://localhost:5001"
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/README.md:
--------------------------------------------------------------------------------
1 | # API Project
2 |
3 | REST API project built with ASP.NET Core 3.1 with endpoints to work with the todo items.
4 |
5 | # Getting started
6 |
7 | **Prerequisites**
8 | * Azure Cosmos DB Emulator
9 | * appsettings.Development.json being updated with db connection strings
10 |
11 | # NOTES
12 | 1. Running the API project will ensure both Cosmos DB containers are created in Cosmos DB Emulator, and Identity database is created in the in-memory database or the SQL Server depending on the setup in DatabaseConfig.cs.
13 | 1. Running the API project will also seed application data and identity data.
14 |
15 | Run API
16 | 1. Run API using Visual Studio
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/Startup.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using CleanArchitectureCosmosDB.Infrastructure.Extensions;
3 | using CleanArchitectureCosmosDB.Infrastructure.Identity;
4 | using CleanArchitectureCosmosDB.Infrastructure.Identity.Models.Authentication;
5 | using CleanArchitectureCosmosDB.WebAPI.Config;
6 | using Microsoft.AspNet.OData.Extensions;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Hosting;
12 | using System.Linq;
13 | using System.Reflection;
14 |
15 | namespace CleanArchitectureCosmosDB.WebAPI
16 | {
17 | ///
18 | /// Start up
19 | ///
20 | public class Startup
21 | {
22 | ///
23 | /// Configuration
24 | ///
25 | public IConfiguration Configuration { get; }
26 |
27 | ///
28 | /// ctor
29 | ///
30 | ///
31 | public Startup(IConfiguration configuration)
32 | {
33 | Configuration = configuration;
34 | }
35 |
36 | ///
37 | /// This method gets called by the runtime. Use this method to add services to the container.
38 | ///
39 | ///
40 | public void ConfigureServices(IServiceCollection services)
41 | {
42 | // Strongly-typed configurations using IOptions
43 | services.Configure(Configuration.GetSection("token"));
44 | services.Configure(Configuration.GetSection("TokenServiceProvider"));
45 |
46 | // Authentication and Authorization
47 | services.SetupAuthentication(Configuration);
48 | services.SetAuthorization();
49 |
50 | // Cosmos DB for application data
51 | services.SetupCosmosDb(Configuration);
52 | // Identity DB for Identity data
53 | services.SetupIdentityDatabase(Configuration);
54 |
55 | // API controllers
56 | services.SetupControllers();
57 |
58 | // HttpContext
59 | services.AddHttpContextAccessor();
60 |
61 | // AutoMapper, this will scan and register everything that inherits AutoMapper.Profile
62 | services.AddAutoMapper(Assembly.GetExecutingAssembly());
63 |
64 | // MediatR for Command/Query pattern and pipeline behaviours
65 | services.SetupMediatr();
66 |
67 | // Caching
68 | services.SetupInMemoryCaching();
69 |
70 | // NSwag Swagger
71 | services.SetupNSwag();
72 |
73 | // OData
74 | services.SetupOData();
75 |
76 | // Blob Storage
77 | // Since storage can be shared among projects, extension method is defined in Infrastructure project.
78 | services.SetupStorage(Configuration);
79 |
80 | }
81 |
82 | ///
83 | /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
84 | ///
85 | ///
86 | ///
87 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
88 | {
89 | if (env.IsDevelopment())
90 | {
91 | app.UseDeveloperExceptionPage();
92 |
93 | // ONLY automatically create development databases
94 | app.EnsureCosmosDbIsCreated();
95 | app.SeedToDoContainerIfEmptyAsync().Wait();
96 | // Optional: auto-create and seed Identity DB
97 | app.EnsureIdentityDbIsCreated();
98 | app.SeedIdentityDataAsync().Wait();
99 | }
100 |
101 | // NSwag Swagger
102 | app.UseOpenApi();
103 | app.UseSwaggerUi3();
104 |
105 | app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
106 | app.UseHttpsRedirection();
107 |
108 | app.UseRouting();
109 |
110 | app.UseAuthorization();
111 |
112 | app.UseEndpoints(endpointRouteBuilder =>
113 | {
114 | endpointRouteBuilder.MapControllers();
115 |
116 | // OData configuration
117 | endpointRouteBuilder.EnableDependencyInjection();
118 | endpointRouteBuilder.Filter().Select().Count().OrderBy();
119 | });
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "CleanArchitectureCosmosDB": {
4 | "EndpointUrl": "https://localhost:8081",
5 | // default primary key used by CosmosDB emulator
6 | "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
7 | "DatabaseName": "CleanArchitectureCosmosDB",
8 | "Containers": [
9 | {
10 | "Name": "Audit",
11 | "PartitionKey": "/EntityId"
12 | },
13 | {
14 | "Name": "Todo",
15 | "PartitionKey": "/Category"
16 | }
17 | ]
18 | },
19 | "CleanArchitectureIdentity": "Server=localhost\\SQLSERVER2016;Database=CleanArchitectureIdentity;Trusted_Connection=True;",
20 | "StorageConnectionString": "azure.blob://emu=true"
21 | },
22 | /* Token Service Provider */
23 | "TokenServiceProvider": {
24 | "Authority": "http://localhost:3000",
25 | "SetPasswordPath": "/verify",
26 | "ResetPasswordPath": "/resetPassword"
27 | },
28 | /* For token issued by application*/
29 | "token": {
30 | "secret": "rqkGzhVj8mne_GN3BREE!A4j7F69dR__tc!48EAG5ZTTQ&eN2m?LVD4g$-N!8xrH+m5!PPZPPE!WqpASHwmkA4Nt2q=&*?WZRzvGrgqkMp29zs7M8sm_V+VLvb7p+H8GSNr7?-_JywP$5cDm653!fH$CPvEzA64^L&AbqEExr7=zBchJLNESK&HeEjwTChT=qRcE$LtpS5%ec%s8qvY?8eEtH#$+xX-Z-Zcpq^n!3q5kZNQMD@5XGZ_7@e#Zy&pT",
31 | "issuer": "https://github.com/ShawnShiSS",
32 | "audience": "audience",
33 | "expiry": 120,
34 | "refreshExpiry": 10080
35 | },
36 | "Serilog": {
37 | "Using": [
38 | "Serilog.Sinks.Console",
39 | "Serilog.Sinks.File"
40 | ],
41 | "MinimumLevel": {
42 | "Default": "Information",
43 | "Override": {
44 | "Microsoft": "Error",
45 | "System": "Error"
46 | }
47 | },
48 | "WriteTo": [
49 | {
50 | "Name": "File",
51 | "Args": {
52 | "path": "C:\\Logs\\todo-api\\log-todo-api-.txt",
53 | "rollingInterval": "Day"
54 | }
55 | },
56 | {
57 | "Name": "Console",
58 | "Args": {
59 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
60 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}"
61 | }
62 | },
63 | {
64 | "Name": "Seq",
65 | "Args": {
66 | "serverUrl": "http://localhost:5341"
67 | }
68 | }
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/appsettings.Production.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "CleanArchitectureCosmosDB": {
4 | "EndpointUrl": "https://localhost:8081",
5 | // default primary key used by CosmosDB emulator
6 | "PrimaryKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
7 | "DatabaseName": "CleanArchitectureCosmosDB",
8 | "Containers": [
9 | {
10 | "Name": "Audit",
11 | "PartitionKey": "/EntityId"
12 | },
13 | {
14 | "Name": "Todo",
15 | "PartitionKey": "/Category"
16 | }
17 | ]
18 | },
19 | "StorageConnectionString": "azure.blob://emu=true"
20 | },
21 | "Serilog": {
22 | "Using": [
23 | "Serilog.Sinks.Console",
24 | "Serilog.Sinks.File"
25 | ],
26 | "MinimumLevel": {
27 | "Default": "Information",
28 | "Override": {
29 | "Microsoft": "Error",
30 | "System": "Error"
31 | }
32 | },
33 | "WriteTo": [
34 | {
35 | "Name": "File",
36 | "Args": {
37 | "path": "D:\\home\\LogFiles\\http\\RawLogs\\log-todo-api-.txt",
38 | "rollingInterval": "Day"
39 | }
40 | },
41 | {
42 | "Name": "Console",
43 | "Args": {
44 | "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
45 | "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception}"
46 | }
47 | }
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/CleanArchitectureCosmosDB.WebAPI/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | }
4 |
--------------------------------------------------------------------------------
/tests/CleanArchitectureCosmosDB.UnitTests/CleanArchitectureCosmosDB.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/CleanArchitectureCosmosDB.UnitTests/Core/Entities/ToDoItemTest.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitectureCosmosDB.Core.Entities;
2 | using Xunit;
3 |
4 | namespace CleanArchitectureCosmosDB.UnitTests.Core.Entities
5 | {
6 | public class ToDoItemTest
7 | {
8 | [Fact]
9 | public void SetIsCompletedToTrue()
10 | {
11 | ToDoItem item = new ToDoItem()
12 | {
13 | Category = "UnitTest",
14 | Title = "Mark me as completed",
15 | //IsCompleted = false // private property that can only be set by MarkComplete method
16 | };
17 |
18 | item.MarkComplete();
19 |
20 | Assert.True(item.IsCompleted);
21 |
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------