Awesome
:warning: Warning
The code samples contain multiple ways and patterns to do things and not always be considered best practices or recommended for all situations.
Database Centric vs Domain Centric Architecture
Hexagonal Architecture
Onion Architecture
The Clean Architecture
Classic Three-layer Architecture
Modern Four-layer Architecture
Layer Dependencies
Layer Examples
Testing Pyramid
Vertical Slice Architecture (Modular Monolith)
Solution Structure
How to Run:
Update Configuration
<details> <summary><b>Additional Configuration Sources</b></summary>-
Open ClassifiedAds.WebMVC/appsettings.json and jump to ConfigurationSources section.
"ConfigurationSources": { "SqlServer": { "IsEnabled": false, "ConnectionString": "Server=127.0.0.1;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=127.0.0.1;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=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SqlQuery": "select [Key], [Value] from ConfigurationEntries" }, "AzureKeyVault": { "IsEnabled": true, "VaultName": "https://xxx.vault.azure.net/" } },
-
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" } },
-
Open below files and jump to MessageBroker section:
- ClassifiedAds.IdentityServer/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.BackgroundServer/appsettings.json
"MessageBroker": { "Provider": "RabbitMQ", }
-
Use RabbitMQ
"MessageBroker": { "Provider": "RabbitMQ", "RabbitMQ": { "HostName": "localhost", "UserName": "guest", "Password": "guest", "ExchangeName": "amq.direct", "RoutingKeys": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" }, "QueueNames": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" } } }
-
Use Kafka:
"MessageBroker": { "Provider": "Kafka", "Kafka": { "BootstrapServers": "localhost:9092", "Topics": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" }, } }
-
Use Azure Queue Storage:
"MessageBroker": { "Provider": "AzureQueue", "AzureQueue": { "ConnectionString": "xxx", "QueueNames": { "FileUploadedEvent": "classifiedadds-fileuploaded", "FileDeletedEvent": "classifiedadds-filedeleted", "EmailMessageCreatedEvent": "classifiedadds-emailcreated", "SmsMessageCreatedEvent": "classifiedadds-smscreated" } } }
-
Use Azure Service Bus:
"MessageBroker": { "Provider": "AzureServiceBus", "AzureServiceBus": { "ConnectionString": "xxx", "QueueNames": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" } } }
-
Use Azure Event Grid:
"MessageBroker": { "Provider": "AzureEventGrid", "AzureEventGrid": { "DomainEndpoint": "https://xxx.xxx-1.eventgrid.azure.net/api/events", "DomainKey": "xxxx", "Topics": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted" "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" } } }
-
Use Azure Event Hubs:
"MessageBroker": { "Provider": "AzureEventHub", "AzureEventHub": { "ConnectionString": "Endpoint=sb://xxx.servicebus.windows.net/;SharedAccessKeyName=xxx;SharedAccessKey=xxx", "Hubs": { "FileUploadedEvent": "classifiedadds_fileuploaded", "FileDeletedEvent": "classifiedadds_filedeleted", "EmailMessageCreatedEvent": "classifiedadds_emailcreated", "SmsMessageCreatedEvent": "classifiedadds_smscreated" }, "StorageConnectionString": "DefaultEndpointsProtocol=https;AccountName=xxx;AccountKey=xxx;EndpointSuffix=core.windows.net", "StorageContainerNames": { "FileUploadedEvent": "eventhub-fileuploaded", "FileDeletedEvent": "eventhub-filedeleted", "EmailMessageCreatedEvent": "eventhub-emailcreated", "SmsMessageCreatedEvent": "eventhub-smscreated" } } }
- Open and jump to Logging section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.IdentityServer/appsettings.json
- ClassifiedAds.BackgroundServer/appsettings.json
"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" } },
- Open and jump to Caching section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.IdentityServer/appsettings.json
"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="5.0" dotnet sql-cache create "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#" dbo CacheEntries
"Caching": { "Distributed": { "Provider": "SqlServer", "SqlServer": { "ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#", "SchemaName": "dbo", "TableName": "CacheEntries" } } },
- Open and jump to Monitoring section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.IdentityServer/appsettings.json
"Monitoring": { "MiniProfiler": { }, "AzureApplicationInsights": { } },
- Use MiniProfiler:
"Monitoring": { "MiniProfiler": { "IsEnabled": true, "SqlServerStorage": { "ConectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#;MultipleActiveResultSets=true;Encrypt=False", "ProfilersTable": "MiniProfilers", "TimingsTable": "MiniProfilerTimings", "ClientTimingsTable": "MiniProfilerClientTimings" } }, },
- Use Azure Application Insights:
"Monitoring": { "AzureApplicationInsights": { "IsEnabled": true, "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "EnableSqlCommandTextInstrumentation": true } },
- Use AppMetrics:
"Monitoring": { "AppMetrics": { "IsEnabled": true, "MetricsOptions": { "DefaultContextLabel": "ClassifiedAds.WebAPI", "Enabled": true, "ReportingEnabled": true }, "MetricsWebTrackingOptions": { "ApdexTrackingEnabled": true, "ApdexTSeconds": 0.1, "IgnoredHttpStatusCodes": [ 404 ], "IgnoredRoutesRegexPatterns": [], "OAuth2TrackingEnabled": true }, "MetricEndpointsOptions": { "MetricsEndpointEnabled": true, "MetricsTextEndpointEnabled": true, "EnvironmentInfoEndpointEnabled": true } } },
- Use Both:
"Monitoring": { "MiniProfiler": { "IsEnabled": true, "SqlServerStorage": { "ConectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#;MultipleActiveResultSets=true;Encrypt=False", "ProfilersTable": "MiniProfilers", "TimingsTable": "MiniProfilerTimings", "ClientTimingsTable": "MiniProfilerClientTimings" } }, "AzureApplicationInsights": { "IsEnabled": true, "InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "EnableSqlCommandTextInstrumentation": true }, "AppMetrics": { "IsEnabled": true, "MetricsOptions": { "DefaultContextLabel": "ClassifiedAds.WebAPI", "Enabled": true, "ReportingEnabled": true }, "MetricsWebTrackingOptions": { "ApdexTrackingEnabled": true, "ApdexTSeconds": 0.1, "IgnoredHttpStatusCodes": [ 404 ], "IgnoredRoutesRegexPatterns": [], "OAuth2TrackingEnabled": true }, "MetricEndpointsOptions": { "MetricsEndpointEnabled": true, "MetricsTextEndpointEnabled": true, "EnvironmentInfoEndpointEnabled": true } } },
- Open and jump to Interceptors section of below files:
- ClassifiedAds.WebAPI/appsettings.json
- ClassifiedAds.WebMVC/appsettings.json
- ClassifiedAds.IdentityServer/appsettings.json
- ClassifiedAds.BackgroundServer/appsettings.json
"Interceptors": { "LoggingInterceptor": true, "ErrorCatchingInterceptor": false },
- 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" },
- Open ClassifiedAds.WebAPI/appsettings.json and jump to CORS section:
"CORS": { "AllowAnyOrigin": false, "AllowedOrigins": [ "http://localhost:4200", "http://localhost:3000", "http://localhost:8080" ] },
- Open ClassifiedAds.NotificationServer/appsettings.json and jump to CORS section:
"CORS": { "AllowedOrigins": [ "https://localhost:44364", "http://host.docker.internal:9003" ] }
- 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" } },
- Open ClassifiedAds.BackgroundServer/appsettings.json and jump to Notification -> Email section:
"Notification": { "Email": { "Provider": "Fake", } }
- Use SmtpClient:
"Notification": { "Email": { "Provider": "SmtpClient", "SmtpClient": { "Host": "localhost", "Port": "", "UserName": "", "Password": "", "EnableSsl": "" } } }
- Open ClassifiedAds.BackgroundServer/appsettings.json and jump to Notification -> Sms section:
"Notification": { "Sms": { "Provider": "Fake", } }
- Use Twilio
"Notification": { "Sms": { "Provider": "Twilio", "Twilio": { "AccountSId": "", "AuthToken": "", "FromNumber": "" } } }
Run or Debug the Solution
-
Web MVC Home Page: https://localhost:44364/
-
Navigate to Health Checks UI https://localhost:44364/healthchecks-ui#/healthchecks and make sure everything is green.
-
Login on Identity Server:
- Option 1: Use default created account:
- User Name: phong@gmail.com
- Password: v*7Un8b4rcN@<-RN
- Option 2: Register new account at https://localhost:44367/Account/Register
- Option 1: Use default created account:
-
Open Blazor Home Page at: https://localhost:44331
How to Build and Run Single Page Applications:
- Angular:
-
Navigate to folder: UIs/angular/
npm install ng serve
-
Update environment.ts & environment.prod.ts
export const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.Angular" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:4200/" };
-
Go to http://localhost:4200/
-
- React:
-
Navigate to folder: UIs/reactjs/
npm install npm run dev
-
Update environment.dev.js & environment.js
const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.React" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:3000/" }; export default environment;
-
Go to http://localhost:3000/
-
- Vue:
- Navigate to folder: UIs/vuejs/
npm install npm run serve
- Update environment.dev.js & environment.dev.js
const environment = { OpenIdConnect: { Authority: "https://localhost:44367", ClientId: "ClassifiedAds.Vue" }, ResourceServer: { Endpoint: "https://localhost:44312/api/" }, CurrentUrl: "http://localhost:8080/" }; export default environment;
- Navigate to folder: UIs/vuejs/
-
Go to http://localhost:8080/
-
Before Login, go to Identity Server https://localhost:44367/Client to make sure application clients have been registered:
How to Run on Docker Containers:
-
Add Migrations if you haven't done on previous steps:
- Install dotnet-ef cli:
dotnet tool install --global dotnet-ef --version="5.0"
- 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
- Install dotnet-ef cli:
-
Navigate to Monolith and run:
docker-compose build docker-compose up
-
Open Web MVC Home Page at: http://host.docker.internal:9003
-
Navigate to Health Checks UI http://host.docker.internal:9003/healthchecks-ui#/healthchecks and make sure everything is green.
-
Login on Identity Server:
- Use default created account: phong@gmail.com / v*7Un8b4rcN@<-RN
- Register new account at http://host.docker.internal:9000/Account/Register
-
Open Blazor Home Page at: http://host.docker.internal:9008
How to Run Integration & End to End Tests:
-
Update ClassifiedAds.IntegrationTests/appsettings.json
{ "OpenIdConnect": { "Authority": "https://localhost:44367", "ClientId": "ClassifiedAds.WebMVC", "ClientSecret": "secret", "RequireHttpsMetadata": "true" }, "WebAPI": { "Endpoint": "https://localhost:44312" }, "GraphQL": { "Endpoint": "https://localhost:44392/graphql" }, "Login": { "UserName": "phong@gmail.com", "Password": "v*7Un8b4rcN@<-RN", "Scope": "ClassifiedAds.WebAPI" } }
-
Download Chrome Driver
-
Update ClassifiedAds.EndToEndTests/appsettings.json
{ "ChromeDriverPath": "D:\\Downloads\\chromedriver_win32\\72", "Login": { "Url": "https://localhost:44364/Home/Login", "UserName": "phong@gmail.com", "Password": "v*7Un8b4rcN@<-RN" } }
Application URLs:
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Application-URLs
Roadmap:
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Roadmap
Licence 🔑
This repository is licensed under the MIT license.
Duende.IdentityServer License 🔑
Duende.IdentityServer is available under both a FOSS (RPL) and a commercial license.
For the production environment, it is necessary to get a specific license, if you would like more information about the licensing of Duende.IdentityServer - please check this link.
The source code under /src/IdentityServer/Duende folder uses the source code from https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI which is under the terms of the following license.
EPPLus License 🔑
EPPlus 5 can be used under Polyform Noncommercial license or a commercial license.
For the production environment, it is necessary to get a specific license, if you would like more information about the licensing of EPPlus 5 - please check this link.