/Practical.CleanArchitecture

Asp.Net Core 3.1 samples (+ Angular 9.0, React 16.13, Vue 2.6) with modern Clean Architecture, Domain-Driven Design, CQRS, Event Sourcing, SOLID, Asp.Net Core Identity Custom Storage, Identity Server 4 Admin UI, Entity Framework Core, Blazor, Selenium E2E Testing, SignalR Notification, Hangfire Tasks Scheduling, Health Checks, Security Headers, ...

Primary LanguageC#

Domain-Driven Design Path | Pluralsight

Database Centric vs Domain Centric Architecture

alt text

(open on draw.io)

Hexagonal Architecture

alt text

(open on draw.io)

Onion Architecture

alt text

(open on draw.io)

The Clean Architecture

alt text

(open on draw.io)

Classic Three-layer Architecture

alt text

(open on draw.io)

Modern Four-layer Architecture

alt text

(open on draw.io)

Layer Dependencies

alt text

(open on draw.io)

Layer Examples

alt text

(open on draw.io)

Solution Structure

alt text

How to Run:

Update Configuration

Database
  • Update Connection Strings:

    Project Configuration File Configuration Key
    ClassifiedAds.Migrator appsettings.json ConnectionStrings:ClassifiedAds
    ClassifiedAds.BackgroundServer appsettings.json ConnectionStrings:ClassifiedAds
    ClassifiedAds.IdentityServer appsettings.json ConnectionStrings:ClassifiedAds
    ClassifiedAds.WebAPI appsettings.json ConnectionStrings:ClassifiedAds
    ClassifiedAds.WebMVC appsettings.json ConnectionStrings:ClassifiedAds
  • Run Migration:

    • Option 1: Using dotnet cli:
      • Install dotnet-ef cli:
        dotnet tool install --global dotnet-ef --version="3.1"
        
      • Navigate to ClassifiedAds.Migrator and run these commands:
        dotnet ef migrations add Init --context AdsDbContext -o Migrations/AdsDb
        dotnet ef migrations add Init --context ConfigurationDbContext -o Migrations/ConfigurationDb
        dotnet ef migrations add Init --context PersistedGrantDbContext -o Migrations/PersistedGrantDb
        dotnet ef migrations add Init --context MiniProfilerDbContext -o Migrations/MiniProfilerDb
        dotnet ef database update --context AdsDbContext
        dotnet ef database update --context ConfigurationDbContext
        dotnet ef database update --context PersistedGrantDbContext
        dotnet ef database update --context MiniProfilerDbContext
        
    • Option 2: Using Package Manager Console:
      • Set ClassifiedAds.Migrator as StartUp Project
      • Open Package Manager Console, select ClassifiedAds.Migrator as Default Project
      • Run these commands:
        Add-Migration -Context AdsDbContext Init -OutputDir Migrations/AdsDb
        Add-Migration -Context ConfigurationDbContext Init -OutputDir Migrations/ConfigurationDb
        Add-Migration -Context PersistedGrantDbContext Init -OutputDir Migrations/PersistedGrantDb
        Add-Migration -Context MiniProfilerDbContext Init -OutputDir Migrations/MiniProfilerDb
        Update-Database -Context AdsDbContext
        Update-Database -Context ConfigurationDbContext
        Update-Database -Context PersistedGrantDbContext
        Update-Database -Context MiniProfilerDbContext
        
Additional Configuration Sources
  • Open ClassifiedAds.WebMVC/appsettings.json and jump to ConfigurationSources section.

    "ConfigurationSources": {
      "SqlServer": {
        "IsEnabled": false,
        "ConnectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#",
        "SqlQuery": "select [Key], [Value] from ConfigurationEntries"
      },
      "AzureKeyVault": {
        "IsEnabled": false,
        "VaultName": "https://xxx.vault.azure.net/"
      }
    },
  • Get from Sql Server database:

    "ConfigurationSources": {
      "SqlServer": {
        "IsEnabled": true,
        "ConnectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#",
        "SqlQuery": "select [Key], [Value] from ConfigurationEntries"
      },
    },
  • Get from Azure Key Vault:

    "ConfigurationSources": {
      "AzureKeyVault": {
        "IsEnabled": true,
        "VaultName": "https://xxx.vault.azure.net/"
      }
    },
  • Use Both:

    "ConfigurationSources": {
      "SqlServer": {
        "IsEnabled": true,
        "ConnectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#",
        "SqlQuery": "select [Key], [Value] from ConfigurationEntries"
      },
      "AzureKeyVault": {
        "IsEnabled": true,
        "VaultName": "https://xxx.vault.azure.net/"
      }
    },
Storage
  • Open ClassifiedAds.WebMVC/appsettings.json, ClassifiedAds.WebAPI/appsettings.json and jump to Storage section.

    "Storage": {
      "Provider": "Local",
    },
  • Use Local Files:

    "Storage": {
      "Provider": "Local",
      "Local": {
        "Path": "E:\\files"
      },
    },
  • Use Azure Blob:

    "Storage": {
      "Provider": "Azure",
      "Azure": {
        "ConnectionString": "xxx",
        "Container": "classifiedadds"
      },
    },
  • Use Amazon S3:

    "Storage": {
      "Provider": "Amazon",
      "Amazon": {
        "AccessKeyID": "xxx",
        "SecretAccessKey": "xxx",
        "BucketName": "classifiedadds",
        "RegionEndpoint": "ap-southeast-1"
      }
    },
Message Broker
  • Open ClassifiedAds.WebMVC/appsettings.json, ClassifiedAds.WebAPI/appsettings.json, ClassifiedAds.BackgroundServer/appsettings.json and jump to MessageBroker section.

    "MessageBroker": {
      "Provider": "RabbitMQ",
    }
  • Use RabbitMQ

    "MessageBroker": {
      "Provider": "RabbitMQ",
      "RabbitMQ": {
        "HostName": "localhost",
        "UserName": "guest",
        "Password": "guest",
        "ExchangeName": "amq.direct",
        "RoutingKey_FileUploaded": "classifiedadds_fileuploaded",
        "RoutingKey_FileDeleted": "classifiedadds_filedeleted",
        "QueueName_FileUploaded": "classifiedadds_fileuploaded",
        "QueueName_FileDeleted": "classifiedadds_filedeleted"
      }
    }
  • Use Kafka:

    "MessageBroker": {
      "Provider": "Kafka",
      "Kafka": {
        "BootstrapServers": "localhost:9092",
        "Topic_FileUploaded": "classifiedadds_fileuploaded",
        "Topic_FileDeleted": "classifiedadds_filedeleted"
      }
    }
  • Use Azure Queue Storage:

    "MessageBroker": {
      "Provider": "AzureQueue",
      "AzureQueue": {
        "ConnectionString": "xxx",
        "QueueName_FileUploaded": "classifiedadds-fileuploaded",
        "QueueName_FileDeleted": "classifiedadds-filedeleted"
      }
    }
  • Use Azure Service Bus:

    "MessageBroker": {
      "Provider": "AzureServiceBus",
      "AzureServiceBus": {
        "ConnectionString": "xxx",
        "QueueName_FileUploaded": "classifiedadds_fileuploaded",
        "QueueName_FileDeleted": "classifiedadds_filedeleted"
      }
    }
  • Use Azure Event Grid:

    "MessageBroker": {
      "Provider": "AzureEventGrid",
      "AzureEventGrid": {
        "DomainEndpoint": "https://xxx.xxx-1.eventgrid.azure.net/api/events",
        "DomainKey": "xxxx",
        "Topic_FileUploaded": "classifiedadds_fileuploaded",
        "Topic_FileDeleted": "classifiedadds_filedeleted"
      }
    }
  • Use Azure Event Hubs:

    "MessageBroker": {
      "Provider": "AzureEventHub",
      "AzureEventHub": {
        "ConnectionString": "Endpoint=sb://xxx.servicebus.windows.net/;SharedAccessKeyName=xxx;SharedAccessKey=xxx",
        "Hub_FileUploaded": "classifiedadds_fileuploaded",
        "Hub_FileDeleted": "classifiedadds_filedeleted",
        "StorageConnectionString": "DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx;EndpointSuffix=core.windows.net",
        "StorageContainerName_FileUploaded": "eventhub-fileuploaded",
        "StorageContainerName_FileDeleted": "eventhub-filedeleted"
      }
    }
Logging
  • Open and jump to Logging section of below files:
    "Logging": {
      "LogLevel": {
        "Default": "Warning"
      },
      "File": {
        "MinimumLogEventLevel": "Information"
      },
      "Elasticsearch": {
        "IsEnabled": false,
        "Host": "http://localhost:9200",
        "IndexFormat": "classifiedads",
        "MinimumLogEventLevel": "Information"
      },
      "EventLog": {
        "IsEnabled": false,
        "LogName": "Application",
        "SourceName": "ClassifiedAds.WebAPI"
      }
    },
  • Write to Local file (./logs/log.txt). Always enabled.
    "Logging": {
      "File": {
        "MinimumLogEventLevel": "Information"
      },
    },
  • Write to Elasticsearch:
    "Logging": {
      "Elasticsearch": {
        "IsEnabled": true,
        "Host": "http://localhost:9200",
        "IndexFormat": "classifiedads",
        "MinimumLogEventLevel": "Information"
      },
    },
  • Write to Windows Event Log (Windows only):
    "Logging": {
      "EventLog": {
        "IsEnabled": true,
        "LogName": "Application",
        "SourceName": "ClassifiedAds.WebAPI"
      }
    },
  • Enable all options:
    "Logging": {
      "LogLevel": {
        "Default": "Warning"
      },	
      "File": {
        "MinimumLogEventLevel": "Information"
      },
      "Elasticsearch": {
        "IsEnabled": true,
        "Host": "http://localhost:9200",
        "IndexFormat": "classifiedads",
        "MinimumLogEventLevel": "Information"
      },
      "EventLog": {
        "IsEnabled": true,
        "LogName": "Application",
        "SourceName": "ClassifiedAds.WebAPI"
      }
    },
Caching
  • Open and jump to Caching section of below files:
    "Caching": {
      "InMemory": {
    
      },
      "Distributed": {
    
      }
    },
  • Configure options for In Memory Cache:
    "Caching": {
      "InMemory": {
        "SizeLimit": null
      },
    },
  • Use In Memory Distributed Cache (For Local Testing):
    "Caching": {
      "Distributed": {
        "Provider": "InMemory",
        "InMemory": {
          "SizeLimit": null
        }
      }
    },
  • Use Redis Distributed Cache:
    "Caching": {
      "Distributed": {
        "Provider": "Redis",
        "Redis": {
          "Configuration": "xxx.redis.cache.windows.net:6380,password=xxx,ssl=True,abortConnect=False",
          "InstanceName": ""
        }
      }
    },
  • Use Sql Server Distributed Cache:
    dotnet tool install --global dotnet-sql-cache --version="3.1"
    dotnet sql-cache create "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#" dbo CacheEntries
    "Caching": {
      "Distributed": {
        "Provider": "SqlServer",
        "SqlServer": {
          "ConnectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#",
          "SchemaName": "dbo",
          "TableName": "CacheEntries"
        }
      }
    },
Monitoring
  • Open and jump to Monitoring section of below files:
    "Monitoring": {
      "MiniProfiler": {
        
      },
      "AzureApplicationInsights": {
        
      }
    },
  • Use MiniProfiler:
    "Monitoring": {
      "MiniProfiler": {
        "IsEnabled": true,
        "SqlServerStorage": {
          "ConectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#;MultipleActiveResultSets=true",
          "ProfilersTable": "MiniProfilers",
          "TimingsTable": "MiniProfilerTimings",
          "ClientTimingsTable": "MiniProfilerClientTimings"
        }
      },
    },
  • Use Azure Application Insights:
    "Monitoring": {
      "AzureApplicationInsights": {
        "IsEnabled": true,
    	"InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    	"EnableSqlCommandTextInstrumentation": true
      }
    },
  • Use Both:
    "Monitoring": {
      "MiniProfiler": {
        "IsEnabled": true,
        "SqlServerStorage": {
          "ConectionString": "Server=.;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#;MultipleActiveResultSets=true",
          "ProfilersTable": "MiniProfilers",
          "TimingsTable": "MiniProfilerTimings",
          "ClientTimingsTable": "MiniProfilerClientTimings"
        }
      },
      "AzureApplicationInsights": {
        "IsEnabled": true,
        "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "EnableSqlCommandTextInstrumentation": true
      }
    },
Interceptors
Security Headers
  • Open ClassifiedAds.WebAPI/appsettings.json and jump to SecurityHeaders section:
    "SecurityHeaders": {
      "Cache-Control": "no-cache, no-store, must-revalidate",
      "Pragma": "no-cache",
      "Expires": "0"
    },
  • Open ClassifiedAds.WebMVC/appsettings.json and jump to SecurityHeaders section:
    "SecurityHeaders": {
      "Content-Security-Policy": "form-action 'self'; frame-ancestors 'none'",
      "Feature-Policy": "camera 'none'",
      "Referrer-Policy": "strict-origin-when-cross-origin",
      "X-Content-Type-Options": "nosniff",
      "X-Frame-Options": "DENY",
      "X-XSS-Protection": "1; mode=block",
      "Cache-Control": "no-cache, no-store, must-revalidate",
      "Pragma": "no-cache",
      "Expires": "0"
    },
Cross-Origin Resource Sharing (CORS)
External Login
  • Open ClassifiedAds.IdentityServer/appsettings.json and jump to ExternalLogin section:
    "ExternalLogin": {
      "AzureActiveDirectory": {
        "IsEnabled": true,
        "Authority": "https://login.microsoftonline.com/<Directory (tenant) ID>",
        "ClientId": "<Application (client) ID",
        "ClientSecret": "xxx"
      },
      "Microsoft": {
        "IsEnabled": true,
        "ClientId": "<Application (client) ID",
        "ClientSecret": "xxx"
      },
      "Google": {
        "IsEnabled": true,
        "ClientId": "xxx",
        "ClientSecret": "xxx"
      },
      "Facebook": {
        "IsEnabled": true,
        "AppId": "xxx",
        "AppSecret": "xxx"
      }
    },

Set Startup Projects

alt text

Run or Debug the Solution

How to Build and Run Single Page Applications:

How to Run on Docker Containers:

Application URLs:

Project Launch URL Docker Container URL Docker Container URL
BackgroundServer https://localhost:44318 http://localhost:9004 http://host.docker.internal:9004
Blazor https://localhost:44331 http://localhost:9008 http://host.docker.internal:9008
IdentityServer https://localhost:44367 http://localhost:9000 http://host.docker.internal:9000
NotificationServer https://localhost:44390 http://localhost:9001 http://host.docker.internal:9001
WebAPI https://localhost:44312 http://localhost:9002 http://host.docker.internal:9002
WebMVC https://localhost:44364 http://localhost:9003 http://host.docker.internal:9003
GraphQL https://localhost:44392 http://localhost:9006 http://host.docker.internal:9006
Ocelot https://localhost:44340 http://localhost:9007 http://host.docker.internal:9007
Angular http://localhost:4200/
React http://localhost:3000/
Vue http://localhost:8080/