Full-stack .Net 8 Clean Architecture (Microservices, Modular Monolith, Monolith), Blazor, Angular 19, React 19, Vue 3.5, BFF with YARP, Domain-Driven Design, CQRS, SOLID, Asp.Net Core Identity Custom Storage, OpenID Connect, Entity Framework Core, OpenTelemetry, SignalR, Hosted Services, Health Checks, Rate Limiting, Clouds (Azure, AWS, GCP)
β οΈ Warning
The code samples contain multiple ways and patterns to do things and not always be considered best practices or recommended for all situations.
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:
"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"
}
}
}
"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"
}
},
"Logging": {
"File": {
"MinimumLogEventLevel": "Information"
},
},
"Logging": {
"Elasticsearch": {
"IsEnabled": true,
"Host": "http://localhost:9200",
"IndexFormat": "classifiedads",
"MinimumLogEventLevel": "Information"
},
},
"Logging": {
"EventLog": {
"IsEnabled": true,
"LogName": "Application",
"SourceName": "ClassifiedAds.WebAPI"
}
},
"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": {
"InMemory": {
},
"Distributed": {
}
},
"Caching": {
"InMemory": {
"SizeLimit": null
},
},
"Caching": {
"Distributed": {
"Provider": "InMemory",
"InMemory": {
"SizeLimit": null
}
}
},
"Caching": {
"Distributed": {
"Provider": "Redis",
"Redis": {
"Configuration": "xxx.redis.cache.windows.net:6380,password=xxx,ssl=True,abortConnect=False",
"InstanceName": ""
}
}
},
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"
}
}
},
"Monitoring": {
"MiniProfiler": {
},
"AzureApplicationInsights": {
}
},
"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"
}
},
},
"Monitoring": {
"AzureApplicationInsights": {
"IsEnabled": true,
"InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"EnableSqlCommandTextInstrumentation": true
}
},
"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
}
}
},
"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
}
}
},
"Interceptors": {
"LoggingInterceptor": true,
"ErrorCatchingInterceptor": false
},
"SecurityHeaders": {
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
},
"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"
},
"CORS": {
"AllowAnyOrigin": false,
"AllowedOrigins": [ "http://localhost:4200", "http://localhost:3000", "http://localhost:8080" ]
},
"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"
}
},
"Notification": {
"Email": {
"Provider": "Fake",
}
}
"Notification": {
"Email": {
"Provider": "SmtpClient",
"SmtpClient": {
"Host": "localhost",
"Port": "",
"UserName": "",
"Password": "",
"EnableSsl": ""
}
}
}
"Notification": {
"Sms": {
"Provider": "Fake",
}
}
"Notification": {
"Sms": {
"Provider": "Twilio",
"Twilio": {
"AccountSId": "",
"AuthToken": "",
"FromNumber": ""
}
}
}
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:
Open Blazor Home Page at: https://localhost:44331
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.tsx & environment.tsx
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:
npm install
npm run dev
const environment = {
OpenIdConnect: {
Authority: "https://localhost:44367",
ClientId: "ClassifiedAds.Vue"
},
ResourceServer: {
Endpoint: "https://localhost:44312/api/"
},
CurrentUrl: "http://localhost:8080/"
};
export default environment;
Go to http://localhost:8080/
Before Login, go to Identity Server https://localhost:44367/Client to make sure application clients have been registered:
Add Migrations if you havenβt done on previous steps:
dotnet tool install --global dotnet-ef --version="5.0"
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
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:
Open Blazor Home Page at: http://host.docker.internal:9008
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": "[email protected]",
"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": "[email protected]",
"Password": "v*7Un8b4rcN@<-RN"
}
}
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Application-URLs
https://github.com/phongnguyend/Practical.CleanArchitecture/wiki/Roadmap
This repository is licensed under the MIT 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 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.