base on Full-stack .Net 8 Clean Architecture (Microservices, Modular Monolith, Monolith), Blazor, Angular 19, React 18, Vue 3, 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, Cloud Services (Azure, AWS, GCP). #
> :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
![alt text](/docs/imgs/database-centrics-vs-domain-centric-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fdatabase-centrics-vs-domain-centric-architecture.drawio)
# Hexagonal Architecture
![alt text](/docs/imgs/hexagonal-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fhexagonal-architecture.drawio)
# Onion Architecture
![alt text](/docs/imgs/onion-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fonion-architecture.drawio)
# The Clean Architecture
![alt text](/docs/imgs/the-clean-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fthe-clean-architecture.drawio)
# Classic Three-layer Architecture
![alt text](/docs/imgs/classic-three-layer-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fclassic-three-layer-architecture.drawio)
# Modern Four-layer Architecture
![alt text](/docs/imgs/modern-four-layer-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fmodern-four-layer-architecture.drawio)
# Layer Dependencies
![alt text](/docs/imgs/layer-dependencies.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Flayer-dependencies.drawio)
# Layer Examples
![alt text](/docs/imgs/layer-examples.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Flayer-examples.drawio)
# Testing Pyramid
![alt text](/docs/imgs/testing-pyramid.png)
![alt text](/docs/imgs/testing-pyramid-unit-tests.png)
![alt text](/docs/imgs/testing-pyramid-integration-e2e-tests.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Ftesting-pyramid.drawio)
# Vertical Slice Architecture (Modular Monolith)
![alt text](/docs/imgs/vertical-slice-architecture.png)
[*(open on draw.io)*](https://www.draw.io/#Uhttps%3A%2F%2Fraw.githubusercontent.com%2Fphongnguyend%2FPractical.CleanArchitecture%2Fmaster%2Fdocs%2Fimgs%2Fvertical-slice-architecture.drawio)
# Solution Structure
![alt text](/docs/imgs/code-solution-structure.png)
![alt text](/docs/imgs/code-solution-structure-modular-monolith.png)
![alt text](/docs/imgs/code-solution-structure-microservices.png)
# How to Run:
## Update Configuration
<details>
<summary><b>Additional Configuration Sources</b></summary>
- Open [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json) and jump to **ConfigurationSources** section.
```js
"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:
```js
"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:
```js
"ConfigurationSources": {
"AzureKeyVault": {
"IsEnabled": true,
"VaultName": "https://xxx.vault.azure.net/"
}
},
```
- Use Both:
```js
"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/"
}
},
```
</details>
<details>
<summary><b>Storage</b></summary>
- Open [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json), [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json) and jump to **Storage** section.
```js
"Storage": {
"Provider": "Local",
},
```
- Use Local Files:
```js
"Storage": {
"Provider": "Local",
"Local": {
"Path": "E:\\files"
},
},
```
- Use Azure Blob:
```js
"Storage": {
"Provider": "Azure",
"Azure": {
"ConnectionString": "xxx",
"Container": "classifiedadds"
},
},
```
- Use Amazon S3:
```js
"Storage": {
"Provider": "Amazon",
"Amazon": {
"AccessKeyID": "xxx",
"SecretAccessKey": "xxx",
"BucketName": "classifiedadds",
"RegionEndpoint": "ap-southeast-1"
}
},
```
</details>
<details>
<summary><b>Message Broker</b></summary>
- Open below files and jump to **MessageBroker** section:
+ [ClassifiedAds.Background/appsettings.json](/src/Monolith/ClassifiedAds.Background/appsettings.json)
```js
"MessageBroker": {
"Provider": "RabbitMQ",
}
```
- Use RabbitMQ
```js
"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:
```js
"MessageBroker": {
"Provider": "Kafka",
"Kafka": {
"BootstrapServers": "localhost:9092",
"Topics": {
"FileUploadedEvent": "classifiedadds_fileuploaded",
"FileDeletedEvent": "classifiedadds_filedeleted",
"EmailMessageCreatedEvent": "classifiedadds_emailcreated",
"SmsMessageCreatedEvent": "classifiedadds_smscreated"
},
}
}
```
- Use Azure Queue Storage:
```js
"MessageBroker": {
"Provider": "AzureQueue",
"AzureQueue": {
"ConnectionString": "xxx",
"QueueNames": {
"FileUploadedEvent": "classifiedadds-fileuploaded",
"FileDeletedEvent": "classifiedadds-filedeleted",
"EmailMessageCreatedEvent": "classifiedadds-emailcreated",
"SmsMessageCreatedEvent": "classifiedadds-smscreated"
}
}
}
```
- Use Azure Service Bus:
```js
"MessageBroker": {
"Provider": "AzureServiceBus",
"AzureServiceBus": {
"ConnectionString": "xxx",
"QueueNames": {
"FileUploadedEvent": "classifiedadds_fileuploaded",
"FileDeletedEvent": "classifiedadds_filedeleted",
"EmailMessageCreatedEvent": "classifiedadds_emailcreated",
"SmsMessageCreatedEvent": "classifiedadds_smscreated"
}
}
}
```
- Use Azure Event Grid:
```js
"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:
```js
"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"
}
}
}
```
</details>
<details>
<summary><b>Logging</b></summary>
- Open and jump to **Logging** section of below files:
+ [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json)
+ [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json)
+ [ClassifiedAds.Background/appsettings.json](/src/Monolith/ClassifiedAds.Background/appsettings.json)
```js
"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.
```js
"Logging": {
"File": {
"MinimumLogEventLevel": "Information"
},
},
```
- Write to Elasticsearch:
```js
"Logging": {
"Elasticsearch": {
"IsEnabled": true,
"Host": "http://localhost:9200",
"IndexFormat": "classifiedads",
"MinimumLogEventLevel": "Information"
},
},
```
- Write to Windows Event Log (Windows only):
```js
"Logging": {
"EventLog": {
"IsEnabled": true,
"LogName": "Application",
"SourceName": "ClassifiedAds.WebAPI"
}
},
```
- Enable all options:
```js
"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"
}
},
```
</details>
<details>
<summary><b>Caching</b></summary>
- Open and jump to **Caching** section of below files:
+ [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json)
+ [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json)
```js
"Caching": {
"InMemory": {
},
"Distributed": {
}
},
```
- Configure options for In Memory Cache:
```js
"Caching": {
"InMemory": {
"SizeLimit": null
},
},
```
- Use In Memory Distributed Cache (For Local Testing):
```js
"Caching": {
"Distributed": {
"Provider": "InMemory",
"InMemory": {
"SizeLimit": null
}
}
},
```
- Use Redis Distributed Cache:
```js
"Caching": {
"Distributed": {
"Provider": "Redis",
"Redis": {
"Configuration": "xxx.redis.cache.windows.net:6380,password=xxx,ssl=True,abortConnect=False",
"InstanceName": ""
}
}
},
```
- Use Sql Server Distributed Cache:
```js
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
```
```js
"Caching": {
"Distributed": {
"Provider": "SqlServer",
"SqlServer": {
"ConnectionString": "Server=127.0.0.1;Database=ClassifiedAds;User Id=sa;Password=sqladmin123!@#",
"SchemaName": "dbo",
"TableName": "CacheEntries"
}
}
},
```
</details>
<details>
<summary><b>Monitoring</b></summary>
- Open and jump to **Monitoring** section of below files:
+ [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json)
+ [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json)
```js
"Monitoring": {
"MiniProfiler": {
},
"AzureApplicationInsights": {
}
},
```
- Use MiniProfiler:
```js
"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:
```js
"Monitoring": {
"AzureApplicationInsights": {
"IsEnabled": true,
"InstrumentationKey": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"EnableSqlCommandTextInstrumentation": true
}
},
```
- Use AppMetrics:
```js
"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:
```js
"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
}
}
},
```
</details>
<details>
<summary><b>Interceptors</b></summary>
- Open and jump to **Interceptors** section of below files:
+ [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json)
+ [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json)
+ [ClassifiedAds.Background/appsettings.json](/src/Monolith/ClassifiedAds.Background/appsettings.json)
```js
"Interceptors": {
"LoggingInterceptor": true,
"ErrorCatchingInterceptor": false
},
```
</details>
<details>
<summary><b>Security Headers</b></summary>
- Open [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json) and jump to **SecurityHeaders** section:
```js
"SecurityHeaders": {
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
},
```
- Open [ClassifiedAds.WebMVC/appsettings.json](/src/Monolith/ClassifiedAds.WebMVC/appsettings.json) and jump to **SecurityHeaders** section:
```js
"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"
},
```
</details>
<details>
<summary><b>Cross-Origin Resource Sharing (CORS)</b></summary>
- Open [ClassifiedAds.WebAPI/appsettings.json](/src/Monolith/ClassifiedAds.WebAPI/appsettings.json) and jump to **CORS** section:
```js
"CORS": {
"AllowAnyOrigin": false,
"AllowedOrigins": [ "http://localhost:4200", "http://localhost:3000", "http://localhost:8080" ]
},
```
</details>
<details>
<summary><b>External Login</b></summary>
- Open [ClassifiedAds.IdentityServer/appsettings.json](/src/Monolith/ClassifiedAds.IdentityServer/appsettings.json) and jump to **ExternalLogin** section:
```js
"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"
}
},
```
</details>
<details>
<summary><b>Sending Email</b></summary>
- Open [ClassifiedAds.Background/appsettings.json](/src/Monolith/ClassifiedAds.Background/appsettings.json) and jump to **Notification -> Email** section:
```js
"Notification": {
"Email": {
"Provider": "Fake",
}
}
```
- Use SmtpClient:
```js
"Notification": {
"Email": {
"Provider": "SmtpClient",
"SmtpClient": {
"Host": "localhost",
"Port": "",
"UserName": "",
"Password": "",
"EnableSsl": ""
}
}
}
```
</details>
<details>
<summary><b>Sending SMS</b></summary>
- Open [ClassifiedAds.Background/appsettings.json](/src/Monolith/ClassifiedAds.Background/appsettings.json) and jump to **Notification -> Sms** section:
```js
"Notification": {
"Sms": {
"Provider": "Fake",
}
}
```
- Use Twilio
```js
"Notification": {
"Sms": {
"Provider": "Twilio",
"Twilio": {
"AccountSId": "",
"AuthToken": "",
"FromNumber": ""
}
}
}
```
</details>
## Run or Debug the Solution
- Web MVC Home Page: https://localhost:44364/
![alt text](/docs/imgs/web-mvc-home-page.png)
- Navigate to Health Checks UI https://localhost:44364/healthchecks-ui#/healthchecks and make sure everything is green.
![alt text](/docs/imgs/health-checks-ui.png)
- Login on Identity Server:
+ Option 1: Use default created account:
+ User Name:
[email protected]
+ Password: v*7Un8b4rcN@<-RN
+ Option 2: Register new account at https://localhost:44367/Account/Register
![alt text](/docs/imgs/identity-server-login-page.png)
- Open Blazor Home Page at: https://localhost:44331
![alt text](/docs/imgs/blazor-home-page.png)
## How to Build and Run Single Page Applications:
- Angular:
+ Navigate to folder: [UIs/angular/](/src/UIs/angular/)
```
npm install
ng serve
```
+ Update [environment.ts](/src/UIs/angular/src/environments/environment.ts) & [environment.prod.ts](/src/UIs/angular/src/environments/environment.prod.ts)
```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/
![alt text](/docs/imgs/angular-home-page.png)
- React:
+ Navigate to folder: [UIs/reactjs/](/src/UIs/reactjs/)
```
npm install
npm run dev
```
+ Update [environment.dev.js](/src/UIs/reactjs/src/environments/environment.dev.js) & [environment.js](/src/UIs/reactjs/src/environments/environment.js)
```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/
![alt text](/docs/imgs/react-home-page.png)
- Vue:
+ Navigate to folder: [UIs/vuejs/](/src/UIs/vuejs/)
```
npm install
npm run serve
```
+ Update [environment.dev.js](/src/UIs/vuejs/environments/environment.dev.js) & [environment.dev.js](/src/UIs/vuejs/environments/environment.js)
```js
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/
![alt text](/docs/imgs/vue-home-page.png)
- Before Login, go to Identity Server https://localhost:44367/Client to make sure application clients have been registered:
![alt text](/docs/imgs/identity-server-clients-page.png)
## 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](/src/Monolith/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
```
- Navigate to [Monolith](/src/Monolith/) and run:
```
docker-compose build
docker-compose up
```
- Open Web MVC Home Page at: http://host.docker.internal:9003
![alt text](/docs/imgs/web-mvc-home-page.png)
- Navigate to Health Checks UI http://host.docker.internal:9003/healthchecks-ui#/healthchecks and make sure everything is green.
![alt text](/docs/imgs/health-checks-ui-container.png)
- Login on Identity Server:
+ Use default created account:
[email protected] / v*7Un8b4rcN@<-RN
+ Register new account at http://host.docker.internal:9000/Account/Register
- Open Blazor Home Page at: http://host.docker.internal:9008
![alt text](/docs/imgs/blazor-home-page.png)
## How to Run Integration & End to End Tests:
- Update [ClassifiedAds.IntegrationTests/appsettings.json](/src/Monolith/ClassifiedAds.IntegrationTests/appsettings.json)
```js
{
"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](https://chromedriver.chromium.org/downloads)
![alt text](/docs/imgs/chrome_driver_path.png)
- Update [ClassifiedAds.EndToEndTests/appsettings.json](/src/Monolith/ClassifiedAds.EndToEndTests/appsettings.json)
```js
{
"ChromeDriverPath": "D:\\Downloads\\chromedriver_win32\\72",
"Login": {
"Url": "https://localhost:44364/Home/Login",
"UserName": "
[email protected]",
"Password": "v*7Un8b4rcN@<-RN"
}
}
```
![alt text](/docs/imgs/run_e2e_tests.gif)
## 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) 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](https://duendesoftware.com/products/identityserver#pricing).
The source code under [/src/IdentityServer/Duende](/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**](https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI/blob/main/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](https://www.epplussoftware.com/en/LicenseOverview).
##
", Assign "at most 3 tags" to the expected json: {"id":"6468","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"