Compare commits

...

133 Commits

Author SHA1 Message Date
Satyendra Hari
47643eb2d0 added docs 2023-07-24 19:39:44 +05:30
Satyendra Hari
7a5e502428 update the port 2023-07-24 18:06:46 +05:30
Satyendra Hari
47d4d3cff6 add to the container 2023-07-24 16:32:36 +05:30
Satyendra Hari
becce91a7c added new module 2023-07-24 16:31:13 +05:30
David Fowler
06d5164532
Remove envoy (#2127)
- Just go through the gateway directly
- We'll need to update the mobile version eventually
2023-06-16 10:40:57 -07:00
David Fowler
67d4c06b6d
Fixed the spa project (#2126)
- Clean up the SPA project (removed dead code)
- Fixed URLs in override file
2023-06-15 08:03:56 -07:00
David Fowler
508fa593d7
Added implict usings (#2125) 2023-06-15 05:25:36 -07:00
David Fowler
3b9560c26e
Clean up usings and controller attributes (#2124)
* Clean up usings and controller attributes
- Removed redundant attributes when the controllers already specifies what the result type is.
- Use StatusCodes instead of HttpStatusCode.
- Clean up namespaces and use type aliases to disambiguate the many DTOs defined.

* Forgot one

* Update src/Services/Webhooks/Webhooks.API/Controllers/WebhooksController.cs

Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com>

---------

Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com>
2023-06-14 20:56:52 -07:00
David Fowler
c7c2d1ca2f
Merge pull request #2123 from eerhardt/FixRedisBasketRepository
Cache JsonSerializerOptions
2023-06-13 13:52:36 -07:00
Eric Erhardt
5d13c097d3 Address PR feedback 2023-06-13 14:41:55 -05:00
Eric Erhardt
42e161d1f9 Use file scoped namespace. 2023-06-13 11:33:29 -05:00
Eric Erhardt
fd76f390ef Cache JsonSerializerOptions
According to https://github.com/dotnet/runtime/issues/65396, using a new JsonSerializerOptions every time JsonSerializer is invoked is a suboptimal pattern.

Fix this by caching JsonSerializerOptions instances.
2023-06-13 11:31:11 -05:00
Tarun Jain
3169a93344
Merge pull request #2107 from dotnet-architecture/davidfowl/common-services
Modernization
2023-05-16 22:54:58 +05:30
David Fowler
df4eb4c124 More code sharing and clean up WebHooks client 2023-05-09 07:05:40 -07:00
Reuben Bond
e4b94decb2 Use Services.Common in WebhookClient 2023-05-09 07:05:40 -07:00
Reuben Bond
6f4ae509f1 Port WebhookClient to WebApplicationBuilder 2023-05-09 07:05:40 -07:00
David Fowler
dbef61fdaf Nuke the cors from the mobile BFF 2023-05-09 07:05:40 -07:00
Reuben Bond
6ebde3fb5c Remove on-start health check in Ordering.BackgroundTasks so that it doesn't crash on startup 2023-05-09 07:05:39 -07:00
Reuben Bond
8f9f9d168f Add libman package to WebMVC to fix site 2023-05-09 07:05:39 -07:00
Reuben Bond
f5218e8087 Start adding connection strings for VS launch profile 2023-05-09 07:05:39 -07:00
Reuben Bond
72c8167ec0 Attempt to migrate Mobile BFF to common services pattern 2023-05-09 07:05:39 -07:00
Reuben Bond
8c9524c771 Reduce double writes in Application.FunctionalTests build 2023-05-09 07:05:39 -07:00
Reuben Bond
9c2b972cc9 Correctly set scopes for AuthorizeCheckOperationFilter 2023-05-09 07:05:39 -07:00
Reuben Bond
3858d7ccf7 Fix integration tests 2023-05-09 07:05:39 -07:00
David Fowler
a87efdef32 Small nit 2023-05-09 07:05:39 -07:00
Reuben Bond
52cdd1645a Fix Handler_sends_no_command_when_order_already_exists test 2023-05-09 07:05:39 -07:00
David Fowler
89a42e6c8f Removed session since its not used 2023-05-09 07:05:39 -07:00
David Fowler
d5533c0ff9 Move the pic controller to a minimal API 2023-05-09 07:05:39 -07:00
David Fowler
8debda5c67 Don't read the file into memory 2023-05-09 07:05:39 -07:00
David Fowler
6ad5e8fb09 Deleted test controller 2023-05-09 07:05:39 -07:00
David Fowler
bdebee70dc Clean up SignIn action 2023-05-09 07:05:39 -07:00
David Fowler
184ed15ef6 Clean up the auth delegating handler 2023-05-09 07:05:39 -07:00
David Fowler
af42aab95e Don't launch the browser for signalr 2023-05-09 07:05:39 -07:00
David Fowler
6933d9997d Forward SignalR requests through the web app 2023-05-09 07:05:39 -07:00
David Fowler
992b58d4bc Fixed cookies 2023-05-09 07:05:39 -07:00
David Fowler
a7cdb1df68 Remove unused code 2023-05-09 07:05:39 -07:00
David Fowler
0a07aea4ff Small refactoring of the helper methods 2023-05-09 07:05:38 -07:00
David Fowler
8c2ca8dbd9 Make WebMVC use the service common helpers 2023-05-09 07:05:38 -07:00
David Fowler
40315c69d7 Make docker use the reverse proxy 2023-05-09 07:05:38 -07:00
David Fowler
62a6f17595 Removed unneeded usings 2023-05-09 07:05:38 -07:00
David Fowler
64a2d69e92 Use YARP native config 2023-05-09 07:05:38 -07:00
David Fowler
69476a3175 Use the default YARP configuration 2023-05-09 07:05:38 -07:00
David Fowler
758d4bbe88 Specify routes in config 2023-05-09 07:05:38 -07:00
David Fowler
bd745121b2 Added comment about allowed origins 2023-05-09 07:05:38 -07:00
David Fowler
84c6b11c69 Remove cors from the signalr service
- The frontend hits the gateway which does the preflight request
2023-05-09 07:05:38 -07:00
David Fowler
561d48bc62 Fix cors and prefix rewriting 2023-05-09 07:05:38 -07:00
David Fowler
6a2fceb57c Fix more urls 2023-05-09 07:05:38 -07:00
David Fowler
c3efea0293 Add YARP to the BFF directly.
- This avoids the extra process hop
2023-05-09 07:05:38 -07:00
David Fowler
30ed0011cb Added YARP as an API gateway
- It will eventually replace envoy
2023-05-09 07:05:38 -07:00
David Fowler
06d74d1658 Small tweaks to make the MVC application run locally 2023-05-09 07:05:38 -07:00
David Fowler
50952bed10 Sort usings and delete web.config 2023-05-09 07:05:38 -07:00
David Fowler
f76a8c61db Make default URLs work 2023-05-09 07:05:38 -07:00
David Fowler
5a2d38575e Fixed identity url 2023-05-09 07:05:38 -07:00
David Fowler
670a9452e1 Remove health check 2023-05-09 07:05:38 -07:00
David Fowler
69b28c6add Added gRPC and HTTP endpoints via config 2023-05-09 07:05:38 -07:00
David Fowler
d0c710ebdc Docker compose works 2023-05-09 07:05:38 -07:00
David Fowler
e1ec790ddf Make more things work with docker compose 2023-05-09 07:05:38 -07:00
David Fowler
6b8992153a Rename extensions to extensions 2023-05-09 07:05:38 -07:00
David Fowler
d086d278e8 Moved repository to top level folder 2023-05-09 07:05:38 -07:00
David Fowler
3c00be38f9 Make BFF work and clean up ports 2023-05-09 07:05:37 -07:00
David Fowler
031996d87c Fixed check 2023-05-09 07:05:37 -07:00
David Fowler
5ea034106c Move extensions to Extensions folder 2023-05-09 07:05:37 -07:00
David Fowler
acd9a6d04b Clean up the identity project and make it use services common 2023-05-09 07:05:37 -07:00
David Fowler
7027967568 More random clean up 2023-05-09 07:05:37 -07:00
David Fowler
e166b28a0a Update the webhooks project to use service common 2023-05-09 07:05:37 -07:00
Reuben Bond
bcb1374d1e Avoid logging exception details twice in a given log, clean up 2023-05-09 07:05:37 -07:00
David Fowler
57d9baf106 Make the payment API use the common code 2023-05-09 07:05:37 -07:00
David Fowler
a37b0430b2 Remove unneeded dependencies 2023-05-09 07:05:37 -07:00
David Fowler
2a4a6abf9b Small tweaks
- Fix the payment profile's launch profile
- Added Services.Common to global usings
2023-05-09 07:05:37 -07:00
David Fowler
c41cd3830c Updated background tasks to use common service logic 2023-05-09 07:05:37 -07:00
David Fowler
0cb6b08300 Clean up the project 2023-05-09 07:05:37 -07:00
David Fowler
9743c83221 Use the common services in the Ordering.SignalRHub project 2023-05-09 07:05:37 -07:00
David Fowler
6e69a2472e Small naming tweaks for consistency 2023-05-09 07:05:37 -07:00
David Fowler
3357c70bc1 Clean up excess in project file 2023-05-09 07:05:37 -07:00
David Fowler
909f08675b Make the ordering API use the commmon services 2023-05-09 07:05:37 -07:00
David Fowler
a41560544c Split redis and health checks for redis 2023-05-09 07:05:37 -07:00
David Fowler
08e7c3424d Delete more cruft
- Remove migration from the tests
2023-05-09 07:05:37 -07:00
David Fowler
7681405eaf More clean up 2023-05-09 07:05:37 -07:00
David Fowler
ccaad9dc20 Removed unneeded project deps 2023-05-09 07:05:37 -07:00
Reuben Bond
cf02e90aad Migrate from ILoggerFactory to ILogger<T> and use Logging Source Generator 2023-05-09 07:05:37 -07:00
David Fowler
5397e8d5c8 Made small tweak to startup 2023-05-09 07:05:36 -07:00
David Fowler
0fd20ee962 More clean up
- Make the catalog API runnable
- Delete some cruft
2023-05-09 07:05:36 -07:00
David Fowler
34fc9496fd Small clean up to error handling code 2023-05-09 07:05:36 -07:00
David Fowler
a381a6923c Removed filters and special errors handling from MVC 2023-05-09 07:05:36 -07:00
David Fowler
3056418c92 Made a health check api 2023-05-09 07:05:36 -07:00
Reuben Bond
917764273b Remove superfluous UseDeveloperExceptionPage() and AddOptions() calls 2023-05-09 07:05:36 -07:00
Reuben Bond
e2d8590a26 Use WebApplicationBuilder in Mobile.Shopping.HttpAggregator 2023-05-09 07:05:36 -07:00
Reuben Bond
f8abb36bc6 Remove non-existent wwwroot from HttpAggregator projects 2023-05-09 07:05:36 -07:00
Reuben Bond
16b63001df Add Identity sections to config and consume (likely broken). Simplify integration tests 2023-05-09 07:05:36 -07:00
Reuben Bond
83200f9331 Fix Dockerfiles to include Services.Common 2023-05-09 07:05:36 -07:00
Reuben Bond
fea08c78bb Finish removing Autofac 2023-05-09 07:05:36 -07:00
Reuben Bond
b9f48faf99 Use simpler syntax for default 2023-05-09 07:05:36 -07:00
Reuben Bond
02c163246b Add EventBus connection string to Catalog.API 2023-05-09 07:05:36 -07:00
Reuben Bond
da024f9812 Remove Program.AppName 2023-05-09 07:05:36 -07:00
Reuben Bond
8da0a81514 Remove Serilog usage 2023-05-09 07:05:35 -07:00
Reuben Bond
5342c86af0 Remove Serilog from Identity.API and clean up 2023-05-09 07:05:35 -07:00
Reuben Bond
3f5f0b94ed Remove Program from Catalog.API 2023-05-09 07:05:35 -07:00
Reuben Bond
7d28625959 Remove Serilog from Web.Shopping.HttpAggregator 2023-05-09 07:05:35 -07:00
David Fowler
d96e4db08c Remove Program 2023-05-09 07:05:35 -07:00
David Fowler
c59e66861f - Add redis health check
- Add health checks on startup
2023-05-09 07:05:35 -07:00
David Fowler
56d47db91e Read from the connection strings section 2023-05-09 07:05:35 -07:00
David Fowler
b48ba7b74b More clean up 2023-05-09 07:05:35 -07:00
David Fowler
41056e54d8 Update grpc reference 2023-05-09 07:05:35 -07:00
David Fowler
45a04e4a6d Make services run individually and outside of docker
- Removed manual port binding code
- Disable Seq and logstash if config is null
- Disable serilog for now
- Remove IIS from some launch profiles
- Clean up some logging.
2023-05-09 07:05:35 -07:00
David Fowler
bff808016e Remove the UseVault and use the existance of the section 2023-05-09 07:05:35 -07:00
David Fowler
48f640088b More clean up 2023-05-09 07:05:35 -07:00
David Fowler
4e743ef666 Not needed 2023-05-09 07:05:35 -07:00
David Fowler
366019aaa3 Use the assembly name instead of the type name 2023-05-09 07:05:35 -07:00
David Fowler
d4319bdd47 Redirect to swagger 2023-05-09 07:05:35 -07:00
David Fowler
c565a8f799 Unify configuration 2023-05-09 07:05:35 -07:00
David Fowler
794c546d2e Fix the catalog tests 2023-05-09 07:05:35 -07:00
David Fowler
f46b03cb36 Use before Map 2023-05-09 07:05:35 -07:00
David Fowler
d1372cba64 First pass at making the catalog API use the common service helpers 2023-05-09 07:05:34 -07:00
David Fowler
7da7e98a55 Configure so tests work again 2023-05-09 07:05:34 -07:00
David Fowler
9d52426a49 Use default logger 2023-05-09 07:05:34 -07:00
David Fowler
57a93f63f0 More customization 2023-05-09 07:05:34 -07:00
David Fowler
8a40e9fb48 More schema 2023-05-09 07:05:34 -07:00
David Fowler
3fee612e68 More tweaks 2023-05-09 07:05:34 -07:00
David Fowler
00fc3d8a65 More defaults 2023-05-09 07:05:34 -07:00
David Fowler
9af6d6342d Initial attempt at making a common service configuration 2023-05-09 07:05:34 -07:00
David Fowler
e7e0eed9cc Fixed remaining tests
- Lots of duplication zomg
2023-05-09 07:05:34 -07:00
David Fowler
c7edd50b38 Fix ordering and basked scenarios 2023-05-09 07:05:34 -07:00
David Fowler
233b6e56c1 Fixed catalog functional tests 2023-05-09 07:05:34 -07:00
David Fowler
d4c2f17c36 Remove whitespace 2023-05-09 07:05:34 -07:00
David Fowler
746e5da7fa Make more tests pass 2023-05-09 07:05:34 -07:00
David Fowler
f3d2843166 Make tests work 2023-05-09 07:05:34 -07:00
Reuben Bond
91247ec52e BAD MISC - playing with tests 2023-05-09 07:05:34 -07:00
Reuben Bond
d62ebcb791 Fix IncludeScopes 2023-05-09 07:05:34 -07:00
Reuben Bond
109853983d Remove gRPC generated code from global usings etc 2023-05-09 07:05:34 -07:00
Reuben Bond
d91da03665 Fix IncludeScopes setting in appsettings.json for WebSPA 2023-05-09 07:05:34 -07:00
Reuben Bond
746363bfd3 Remove accidentally committed keys and update .gitignore to prevent them being added again 2023-05-09 07:05:34 -07:00
Tarun Jain
3ae0cefbcf
Merge pull request #2105 from ReubenBond/rebond/1
Fix brace placement
2023-05-08 14:40:50 +05:30
Reuben Bond
451d79f7b9 Fix formatting 2023-05-04 06:48:37 -07:00
376 changed files with 5952 additions and 16877 deletions

1
.gitignore vendored
View File

@ -282,3 +282,4 @@ src/**/app.yaml
src/**/inf.yaml src/**/inf.yaml
.angular/ .angular/
/src/Services/Identity/Identity.API/keys/*.json

BIN
gothrough docs.docx Normal file

Binary file not shown.

View File

@ -6,15 +6,15 @@
# Use this values to run the app locally in Windows # Use this values to run the app locally in Windows
ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal
ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5202/c/api/v1/catalog/items/[0]/pic/ ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5121/c/api/v1/catalog/items/[0]/pic/
# Use this values to run the app locally in Mac # Use this values to run the app locally in Mac
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.mac.localhost # ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.mac.localhost
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5202/c/api/v1/catalog/items/[0]/pic/ # ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5121/c/api/v1/catalog/items/[0]/pic/
# Use this values to run the app locally in Linux # Use this values to run the app locally in Linux
# ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.linux.localhost # ESHOP_EXTERNAL_DNS_NAME_OR_IP=docker.for.linux.localhost
# ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5202/c/api/v1/catalog/items/[0]/pic/ # ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5121/c/api/v1/catalog/items/[0]/pic/
# Configure this values to the cloud storage locations # Configure this values to the cloud storage locations
# ESHOP_STORAGE_CATALOG_URL=<YourAzureStorage_Catalog_BLOB_URL> # ESHOP_STORAGE_CATALOG_URL=<YourAzureStorage_Catalog_BLOB_URL>

View File

@ -1,139 +0,0 @@
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: eshop_backend_route
virtual_hosts:
- name: eshop_backend
domains:
- "*"
routes:
- name: "c-short"
match:
prefix: "/c/"
route:
auto_host_rewrite: true
prefix_rewrite: "/catalog-api/"
cluster: catalog
- name: "c-long"
match:
prefix: "/catalog-api/"
route:
auto_host_rewrite: true
cluster: catalog
- name: "o-short"
match:
prefix: "/o/"
route:
auto_host_rewrite: true
prefix_rewrite: "/ordering-api/"
cluster: ordering
- name: "o-long"
match:
prefix: "/ordering-api/"
route:
auto_host_rewrite: true
cluster: ordering
- name: "h-long"
match:
prefix: "/hub/notificationhub"
route:
auto_host_rewrite: true
cluster: signalr-hub
timeout: 300s
- name: "b-short"
match:
prefix: "/b/"
route:
auto_host_rewrite: true
prefix_rewrite: "/basket-api/"
cluster: basket
- name: "b-long"
match:
prefix: "/basket-api/"
route:
auto_host_rewrite: true
cluster: basket
- name: "agg"
match:
prefix: "/"
route:
auto_host_rewrite: true
prefix_rewrite: "/"
cluster: shoppingagg
http_filters:
- name: envoy.router
access_log:
- name: envoy.file_access_log
filter:
not_health_check_filter: {}
config:
json_format:
time: "%START_TIME%"
protocol: "%PROTOCOL%"
duration: "%DURATION%"
request_method: "%REQ(:METHOD)%"
request_host: "%REQ(HOST)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
response_flags: "%RESPONSE_FLAGS%"
route_name: "%ROUTE_NAME%"
upstream_host: "%UPSTREAM_HOST%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
path: "/tmp/access.log"
clusters:
- name: shoppingagg
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: mobileshoppingagg
port_value: 80
- name: catalog
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: catalog-api
port_value: 80
- name: basket
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: basket-api
port_value: 80
- name: ordering
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: ordering-api
port_value: 80
- name: signalr-hub
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: ordering-signalrhub
port_value: 80

View File

@ -1,142 +0,0 @@
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: eshop_backend_route
virtual_hosts:
- name: eshop_backend
domains:
- "*"
routes:
- name: "c-short"
match:
prefix: "/c/"
route:
auto_host_rewrite: true
prefix_rewrite: "/catalog-api/"
cluster: catalog
- name: "c-long"
match:
prefix: "/catalog-api/"
route:
auto_host_rewrite: true
cluster: catalog
- name: "o-short"
match:
prefix: "/o/"
route:
auto_host_rewrite: true
prefix_rewrite: "/ordering-api/"
cluster: ordering
- name: "o-long"
match:
prefix: "/ordering-api/"
route:
auto_host_rewrite: true
cluster: ordering
- name: "h-long"
match:
prefix: "/hub/notificationhub"
route:
auto_host_rewrite: true
cluster: signalr-hub
timeout: 300s
upgrade_configs:
upgrade_type: "websocket"
enabled: true
- name: "b-short"
match:
prefix: "/b/"
route:
auto_host_rewrite: true
prefix_rewrite: "/basket-api/"
cluster: basket
- name: "b-long"
match:
prefix: "/basket-api/"
route:
auto_host_rewrite: true
cluster: basket
- name: "agg"
match:
prefix: "/"
route:
auto_host_rewrite: true
prefix_rewrite: "/"
cluster: shoppingagg
http_filters:
- name: envoy.router
access_log:
- name: envoy.file_access_log
filter:
not_health_check_filter: {}
config:
json_format:
time: "%START_TIME%"
protocol: "%PROTOCOL%"
duration: "%DURATION%"
request_method: "%REQ(:METHOD)%"
request_host: "%REQ(HOST)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
response_flags: "%RESPONSE_FLAGS%"
route_name: "%ROUTE_NAME%"
upstream_host: "%UPSTREAM_HOST%"
upstream_cluster: "%UPSTREAM_CLUSTER%"
upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
path: "/tmp/access.log"
clusters:
- name: shoppingagg
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: webshoppingagg
port_value: 80
- name: catalog
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: catalog-api
port_value: 80
- name: basket
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: basket-api
port_value: 80
- name: ordering
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: ordering-api
port_value: 80
- name: signalr-hub
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
hosts:
- socket_address:
address: ordering-signalrhub
port_value: 80

View File

@ -16,8 +16,7 @@ public class BasketController : ControllerBase
[HttpPost] [HttpPost]
[HttpPut] [HttpPut]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
{ {
if (data.Items == null || !data.Items.Any()) if (data.Items == null || !data.Items.Any())
@ -73,8 +72,7 @@ public class BasketController : ControllerBase
[HttpPut] [HttpPut]
[Route("items")] [Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{ {
if (!data.Updates.Any()) if (!data.Updates.Any())
@ -110,8 +108,8 @@ public class BasketController : ControllerBase
[HttpPost] [HttpPost]
[Route("items")] [Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
{ {
if (data == null || data.Quantity == 0) if (data == null || data.Quantity == 0)

View File

@ -1,11 +0,0 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers;
[Route("")]
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index()
{
return new RedirectResult("~/swagger");
}
}

View File

@ -16,8 +16,7 @@ public class OrderController : ControllerBase
[Route("draft/{basketId}")] [Route("draft/{basketId}")]
[HttpGet] [HttpGet]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{ {
if (string.IsNullOrEmpty(basketId)) if (string.IsNullOrEmpty(basketId))

View File

@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"

View File

@ -0,0 +1,62 @@
internal static class Extensions
{
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration)
{
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy"));
return services;
}
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" });
return services;
}
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Register delegating handlers
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
// Register http services
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
{
services.AddTransient<GrpcExceptionInterceptor>();
services.AddScoped<IBasketService, BasketService>();
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
{
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
options.Address = new Uri(basketApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<ICatalogService, CatalogService>();
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
{
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
options.Address = new Uri(catalogApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<IOrderingService, OrderingService>();
services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
{
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
options.Address = new Uri(orderingApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
return services;
}
}

View File

@ -1,33 +0,0 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters
{
namespace Basket.API.Infrastructure.Filters
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
}
};
}
}
}
}

View File

@ -1,41 +1,13 @@
global using CatalogApi; global using System.Text.Json;
global using Grpc.Core.Interceptors; global using CatalogApi;
global using Grpc.Core; global using Grpc.Core;
global using Grpc.Core.Interceptors;
global using GrpcBasket; global using GrpcBasket;
global using GrpcOrdering;
global using HealthChecks.UI.Client;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authentication;
global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options; global using Microsoft.Extensions.Options;
global using Microsoft.OpenApi.Models; global using Services.Common;
global using Serilog;
global using Swashbuckle.AspNetCore.SwaggerGen;
global using System.Collections.Generic;
global using System.IdentityModel.Tokens.Jwt;
global using System.Linq;
global using System.Net.Http.Headers;
global using System.Net.Http;
global using System.Net;
global using System.Text.Json;
global using System.Threading.Tasks;
global using System.Threading;
global using System;
global using Microsoft.IdentityModel.Tokens;

View File

@ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor
} }
catch (RpcException e) catch (RpcException e)
{ {
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status);
return default; return default;
} }
} }

View File

@ -1,44 +0,0 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger;
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Version = new System.Version(2, 0);
request.Method = HttpMethod.Get;
var authorizationHeader = _httpContextAccessor.HttpContext
.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authorizationHeader))
{
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
}
var token = await GetToken();
if (token != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
async Task<string> GetToken()
{
const string ACCESS_TOKEN = "access_token";
return await _httpContextAccessor.HttpContext
.GetTokenAsync(ACCESS_TOKEN);
}
}

View File

@ -5,29 +5,19 @@
<AssemblyName>Mobile.Shopping.HttpAggregator</AssemblyName> <AssemblyName>Mobile.Shopping.HttpAggregator</AssemblyName>
<RootNamespace>Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator</RootNamespace>
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath> <DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> <ImplicitUsings>enable</ImplicitUsings>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\" /> <PackageReference Include="Yarp.ReverseProxy" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" /> <PackageReference Include="AspNetCore.HealthChecks.Uris" />
<PackageReference Include="Google.Protobuf" /> <PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" /> <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
<PackageReference Include="Grpc.Core" />
<PackageReference Include="Grpc.Net.ClientFactory" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" /> <PackageReference Include="Grpc.Tools" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> </ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,6 @@
public class UpdateBasketItemsRequest public class UpdateBasketItemsRequest
{ {
public string BasketId { get; set; } public string BasketId { get; set; }
public ICollection<UpdateBasketItemData> Updates { get; set; } public ICollection<UpdateBasketItemData> Updates { get; set; }

View File

@ -1,23 +1,24 @@
await BuildWebHost(args).RunAsync(); var builder = WebApplication.CreateBuilder(args);
IWebHost BuildWebHost(string[] args) =>
WebHost builder.AddServiceDefaults();
.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(cb => builder.Services.AddReverseProxy(builder.Configuration);
{ builder.Services.AddControllers();
var sources = cb.Sources;
sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() builder.Services.AddHealthChecks(builder.Configuration);
{
Optional = true, builder.Services.AddApplicationServices();
Path = "appsettings.localhost.json", builder.Services.AddGrpcServices();
ReloadOnChange = false
}); builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls"));
})
.UseStartup<Startup>() var app = builder.Build();
.UseSerilog((builderContext, config) =>
{ app.UseServiceDefaults();
config
.MinimumLevel.Information() app.UseHttpsRedirection();
.Enrich.FromLogContext()
.WriteTo.Console(); app.MapControllers();
}) app.MapReverseProxy();
.Build();
await app.RunAsync();

View File

@ -5,5 +5,4 @@ public interface IBasketService
Task<BasketData> GetByIdAsync(string id); Task<BasketData> GetByIdAsync(string id);
Task UpdateAsync(BasketData currentBasket); Task UpdateAsync(BasketData currentBasket);
} }

View File

@ -23,9 +23,6 @@ public class OrderApiClient : IOrderApiClient
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); var ordersDraftResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions);
{
PropertyNameCaseInsensitive = true
});
} }
} }

View File

@ -2,10 +2,10 @@
public class OrderingService : IOrderingService public class OrderingService : IOrderingService
{ {
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient;
private readonly ILogger<OrderingService> _logger; private readonly ILogger<OrderingService> _logger;
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{ {
_orderingGrpcClient = orderingGrpcClient; _orderingGrpcClient = orderingGrpcClient;
_logger = logger; _logger = logger;
@ -48,14 +48,14 @@ public class OrderingService : IOrderingService
return data; return data;
} }
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{ {
var command = new CreateOrderDraftCommand var command = new GrpcOrdering.CreateOrderDraftCommand
{ {
BuyerId = basketData.BuyerId, BuyerId = basketData.BuyerId,
}; };
basketData.Items.ForEach(i => command.Items.Add(new BasketItem basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem
{ {
Id = i.Id, Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice, OldUnitPrice = (double)i.OldUnitPrice,

View File

@ -1,210 +0,0 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" })
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
services.AddCustomMvc(Configuration)
.AddCustomAuthentication(Configuration)
.AddHttpServices()
.AddGrpcServices();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase);
app.UsePathBase(pathBase);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger().UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Purchase BFF V1");
c.OAuthClientId("mobileshoppingaggswaggerui");
c.OAuthClientSecret(string.Empty);
c.OAuthRealm(string.Empty);
c.OAuthAppName("Purchase BFF Swagger UI");
});
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
});
}
}
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions();
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
services.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
services.AddSwaggerGen(options =>
{
//options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Shopping Aggregator for Mobile Clients",
Version = "v1",
Description = "Shopping Aggregator for Mobile Clients"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" }
}
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed((host) => true)
.AllowCredentials());
});
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = configuration.GetValue<string>("urls:identity");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "mobileshoppingagg";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
return services;
}
public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "mobileshoppingagg");
});
});
return services;
}
public static IServiceCollection AddHttpServices(this IServiceCollection services)
{
//register delegating handlers
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//register http services
services.AddHttpClient<IOrderApiClient, OrderApiClient>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
{
services.AddTransient<GrpcExceptionInterceptor>();
services.AddScoped<IBasketService, BasketService>();
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
{
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
options.Address = new Uri(basketApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<ICatalogService, CatalogService>();
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
{
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
options.Address = new Uri(catalogApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<IOrderingService, OrderingService>();
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) =>
{
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
options.Address = new Uri(orderingApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
return services;
}
}

View File

@ -1,15 +1,138 @@
{ {
"Logging": { "Logging": {
"IncludeScopes": false, "LogLevel": {
"Debug": { "Default": "Information",
"LogLevel": { "Microsoft.AspNetCore": "Warning",
"Default": "Warning" "System.Net.Http": "Warning"
}
},
"OpenApi": {
"Endpoint": {
"Name": "Purchase BFF V1"
},
"Document": {
"Description": "Shopping Aggregator for Mobile Clients",
"Title": "Shopping Aggregator for Mobile Clients",
"Version": "v1"
},
"Auth": {
"ClientId": "mobileshoppingaggswaggerui",
"AppName": "Mobile shopping BFF Swagger UI"
}
},
"Identity": {
"Url": "http://localhost:5223",
"Audience": "mobileshoppingagg",
"Scopes": {
"webshoppingagg": "Shopping Aggregator for Mobile Clients"
}
},
"ReverseProxy": {
"Routes": {
"c-short": {
"ClusterId": "catalog",
"Match": {
"Path": "c/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/c" }
]
},
"c-long": {
"ClusterId": "catalog",
"Match": {
"Path": "catalog-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/catalog-api" }
]
},
"b-short": {
"ClusterId": "basket",
"Match": {
"Path": "b/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/b" }
]
},
"b-long": {
"ClusterId": "basket",
"Match": {
"Path": "basket-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/basket-api" }
]
},
"o-short": {
"ClusterId": "orders",
"Match": {
"Path": "o/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/o" }
]
},
"o-long": {
"ClusterId": "orders",
"Match": {
"Path": "ordering-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/ordering-api" }
]
},
"h-long": {
"ClusterId": "signalr",
"Match": {
"Path": "hub/notificationhub/{**catch-all}"
}
} }
}, },
"Console": { "Clusters": {
"LogLevel": { "basket": {
"Default": "Warning" "Destinations": {
"destination0": {
"Address": "http://localhost:5221"
}
}
},
"catalog": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5222"
}
}
},
"orders": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5224"
}
}
},
"signalr": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5225"
}
}
} }
} }
} },
"Urls": {
"Basket": "http://localhost:5221",
"Catalog": "http://localhost:5222",
"Orders": "http://localhost:5224",
"Identity": "http://localhost:5223",
"Signalr": "http://localhost:5225",
"GrpcBasket": "http://localhost:6221",
"GrpcCatalog": "http://localhost:6222",
"GrpcOrdering": "http://localhost:6224"
},
"CatalogUrlHC": "http://localhost:5222/hc",
"OrderingUrlHC": "http://localhost:5224/hc",
"BasketUrlHC": "http://localhost:5221/hc",
"IdentityUrlHC": "http://localhost:5223/hc"
} }

View File

@ -8,16 +8,19 @@
"grpcCatalog": "http://localhost:81", "grpcCatalog": "http://localhost:81",
"grpcOrdering": "http://localhost:5581" "grpcOrdering": "http://localhost:5581"
}, },
"IdentityUrlExternal": "http://localhost:5105", "Identity": {
"IdentityUrl": "http://localhost:5105", "ExternalUrl": "http://localhost:5105",
"Url": "http://localhost:5105",
},
"Logging": { "Logging": {
"IncludeScopes": false,
"Debug": { "Debug": {
"IncludeScopes": false,
"LogLevel": { "LogLevel": {
"Default": "Debug" "Default": "Debug"
} }
}, },
"Console": { "Console": {
"IncludeScopes": false,
"LogLevel": { "LogLevel": {
"Default": "Debug" "Default": "Debug"
} }

View File

@ -16,8 +16,7 @@ public class BasketController : ControllerBase
[HttpPost] [HttpPost]
[HttpPut] [HttpPut]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data) public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
{ {
if (data.Items == null || !data.Items.Any()) if (data.Items == null || !data.Items.Any())
@ -74,8 +73,7 @@ public class BasketController : ControllerBase
[HttpPut] [HttpPut]
[Route("items")] [Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data) public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{ {
if (!data.Updates.Any()) if (!data.Updates.Any())
@ -109,8 +107,8 @@ public class BasketController : ControllerBase
[HttpPost] [HttpPost]
[Route("items")] [Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data) public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
{ {
if (data == null || data.Quantity == 0) if (data == null || data.Quantity == 0)

View File

@ -1,11 +0,0 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers;
[Route("")]
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index()
{
return new RedirectResult("~/swagger");
}
}

View File

@ -16,8 +16,7 @@ public class OrderController : ControllerBase
[Route("draft/{basketId}")] [Route("draft/{basketId}")]
[HttpGet] [HttpGet]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId) public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{ {
if (string.IsNullOrWhiteSpace(basketId)) if (string.IsNullOrWhiteSpace(basketId))

View File

@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"

View File

@ -0,0 +1,63 @@
internal static class Extensions
{
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration)
{
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy"));
return services;
}
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" })
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" });
return services;
}
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Register delegating handlers
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
// Register http services
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
{
services.AddTransient<GrpcExceptionInterceptor>();
services.AddScoped<IBasketService, BasketService>();
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
{
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
options.Address = new Uri(basketApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<ICatalogService, CatalogService>();
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
{
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
options.Address = new Uri(catalogApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<IOrderingService, OrderingService>();
services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) =>
{
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
options.Address = new Uri(orderingApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
return services;
}
}

View File

@ -1,34 +0,0 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters
{
namespace Basket.API.Infrastructure.Filters
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
}
};
}
}
}
}

View File

@ -1,41 +1,13 @@
global using CatalogApi; global using System.Text.Json;
global using Grpc.Core.Interceptors; global using CatalogApi;
global using Grpc.Core; global using Grpc.Core;
global using Grpc.Core.Interceptors;
global using GrpcBasket; global using GrpcBasket;
global using GrpcOrdering;
global using HealthChecks.UI.Client;
global using Microsoft.AspNetCore.Authentication;
global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options; global using Microsoft.Extensions.Options;
global using Microsoft.OpenApi.Models; global using Services.Common;
global using Serilog;
global using Swashbuckle.AspNetCore.SwaggerGen;
global using System.Collections.Generic;
global using System.IdentityModel.Tokens.Jwt;
global using System.Linq;
global using System.Net.Http.Headers;
global using System.Net.Http;
global using System.Net;
global using System.Text.Json;
global using System.Threading.Tasks;
global using System.Threading;
global using System;
global using Microsoft.IdentityModel.Tokens;
global using Serilog.Context;

View File

@ -28,7 +28,7 @@ public class GrpcExceptionInterceptor : Interceptor
} }
catch (RpcException e) catch (RpcException e)
{ {
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message); _logger.LogError(e, "Error calling via gRPC: {Status}", e.Status);
return default; return default;
} }
} }

View File

@ -1,40 +0,0 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
public class HttpClientAuthorizationDelegatingHandler
: DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authorizationHeader = _httpContextAccessor.HttpContext
.Request.Headers["Authorization"];
if (!string.IsNullOrWhiteSpace(authorizationHeader))
{
request.Headers.Add("Authorization", new List<string>() { authorizationHeader });
}
var token = await GetTokenAsync();
if (token != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
return await base.SendAsync(request, cancellationToken);
}
Task<string> GetTokenAsync()
{
const string ACCESS_TOKEN = "access_token";
return _httpContextAccessor.HttpContext
.GetTokenAsync(ACCESS_TOKEN);
}
}

View File

@ -1,208 +1,38 @@
var appName = "Web.Shopping.HttpAggregator"; var builder = WebApplication.CreateBuilder(args);
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddReverseProxy(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddHealthChecks(builder.Configuration);
builder.Services.AddCors(options =>
{
// TODO: Read allowed origins from configuration
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
builder.Services.AddApplicationServices();
builder.Services.AddGrpcServices();
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls"));
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration));
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddUrlGroup(new Uri(builder.Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" })
.AddUrlGroup(new Uri(builder.Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" })
.AddUrlGroup(new Uri(builder.Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" })
.AddUrlGroup(new Uri(builder.Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" })
.AddUrlGroup(new Uri(builder.Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" });
builder.Services.AddCustomMvc(builder.Configuration)
.AddCustomAuthentication(builder.Configuration)
.AddApplicationServices()
.AddGrpcServices();
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment())
{ app.UseServiceDefaults();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var pathBase = builder.Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseSwagger().UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Purchase BFF V1");
c.OAuthClientId("webshoppingaggswaggerui");
c.OAuthClientSecret(string.Empty);
c.OAuthRealm(string.Empty);
c.OAuthAppName("web shopping bff Swagger UI");
});
app.UseRouting();
app.UseCors("CorsPolicy"); app.UseCors("CorsPolicy");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapControllers(); app.MapControllers();
app.MapHealthChecks("/hc", new HealthCheckOptions() app.MapReverseProxy();
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/liveness", new HealthCheckOptions
{
Predicate = r => r.Name.Contains("self")
});
try await app.RunAsync();
{
Log.Information("Starts Web Application ({ApplicationContext})...", Program.AppName);
await app.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
return 1;
}
finally
{
Log.CloseAndFlush();
}
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration)
{
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
var logstashUrl = configuration["Serilog:LogstashgUrl"];
return new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.WithProperty("ApplicationContext", Program.AppName)
.Enrich.FromLogContext()
.WriteTo.Console()
.ReadFrom.Configuration(configuration)
.CreateLogger();
}
public partial class Program
{
public static string Namespace = typeof(Program).Assembly.GetName().Name;
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
}
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = configuration.GetValue<string>("urls:identity");
services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "webshoppingagg";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
return services;
}
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions();
services.Configure<UrlsConfig>(configuration.GetSection("urls"));
services.AddControllers()
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Shopping Aggregator for Web Clients",
Version = "v1",
Description = "Shopping Aggregator for Web Clients"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "webshoppingagg", "Shopping Aggregator for Web Clients" }
}
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
return services;
}
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
//register delegating handlers
services.AddTransient<HttpClientAuthorizationDelegatingHandler>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
//register http services
services.AddHttpClient<IOrderApiClient, OrderApiClient>()
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services)
{
services.AddTransient<GrpcExceptionInterceptor>();
services.AddScoped<IBasketService, BasketService>();
services.AddGrpcClient<Basket.BasketClient>((services, options) =>
{
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket;
options.Address = new Uri(basketApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<ICatalogService, CatalogService>();
services.AddGrpcClient<Catalog.CatalogClient>((services, options) =>
{
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog;
options.Address = new Uri(catalogApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
services.AddScoped<IOrderingService, OrderingService>();
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) =>
{
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering;
options.Address = new Uri(orderingApi);
}).AddInterceptor<GrpcExceptionInterceptor>();
return services;
}
}

View File

@ -1,29 +1,12 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:57425/",
"sslPort": 0
}
},
"profiles": { "profiles": {
"IIS Express": { "Web.Shopping.HttpAggregator": {
"commandName": "IISExpress", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "api/values", "applicationUrl": "http://localhost:5229/",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
},
"PurchaseForMvc": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:61632/"
} }
} }
} }

View File

@ -10,7 +10,7 @@ public class BasketService : IBasketService
_basketClient = basketClient; _basketClient = basketClient;
_logger = logger; _logger = logger;
} }
public async Task<BasketData> GetByIdAsync(string id) public async Task<BasketData> GetByIdAsync(string id)
{ {
_logger.LogDebug("grpc client created, request = {@id}", id); _logger.LogDebug("grpc client created, request = {@id}", id);

View File

@ -23,9 +23,6 @@ public class OrderApiClient : IOrderApiClient
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); var ordersDraftResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions);
{
PropertyNameCaseInsensitive = true
});
} }
} }

View File

@ -2,10 +2,10 @@
public class OrderingService : IOrderingService public class OrderingService : IOrderingService
{ {
private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient;
private readonly ILogger<OrderingService> _logger; private readonly ILogger<OrderingService> _logger;
public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger) public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{ {
_orderingGrpcClient = orderingGrpcClient; _orderingGrpcClient = orderingGrpcClient;
_logger = logger; _logger = logger;
@ -48,14 +48,14 @@ public class OrderingService : IOrderingService
return data; return data;
} }
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData) private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{ {
var command = new CreateOrderDraftCommand var command = new GrpcOrdering.CreateOrderDraftCommand
{ {
BuyerId = basketData.BuyerId, BuyerId = basketData.BuyerId,
}; };
basketData.Items.ForEach(i => command.Items.Add(new BasketItem basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem
{ {
Id = i.Id, Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice, OldUnitPrice = (double)i.OldUnitPrice,

View File

@ -4,31 +4,20 @@
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<AssemblyName>Web.Shopping.HttpAggregator</AssemblyName> <AssemblyName>Web.Shopping.HttpAggregator</AssemblyName>
<RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath> <DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="wwwroot\" /> <PackageReference Include="Yarp.ReverseProxy" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" /> <PackageReference Include="AspNetCore.HealthChecks.Uris" />
<PackageReference Include="Google.Protobuf" /> <PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" /> <PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
<PackageReference Include="Grpc.Core" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Grpc.Net.ClientFactory" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" /> <PackageReference Include="Grpc.Tools" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> </ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> <ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Swashbuckle.AspNetCore" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,15 +1,8 @@
{ {
"Logging": { "Logging": {
"IncludeScopes": false, "LogLevel": {
"Debug": { "Default": "Information",
"LogLevel": { "Microsoft.AspNetCore": "Warning"
"Default": "Debug"
}
},
"Console": {
"LogLevel": {
"Default": "Debug"
}
} }
} }
} }

View File

@ -1,15 +1,138 @@
{ {
"Logging": { "Logging": {
"IncludeScopes": false, "LogLevel": {
"Debug": { "Default": "Information",
"LogLevel": { "Microsoft.AspNetCore": "Warning",
"Default": "Warning" "System.Net.Http": "Warning"
}
},
"OpenApi": {
"Endpoint": {
"Name": "Purchase BFF V1"
},
"Document": {
"Description": "Shopping Aggregator for Web Clients",
"Title": "Shopping Aggregator for Web Clients",
"Version": "v1"
},
"Auth": {
"ClientId": "webshoppingaggswaggerui",
"AppName": "Web Shopping BFF Swagger UI"
}
},
"Identity": {
"Url": "http://localhost:5223",
"Audience": "webshoppingagg",
"Scopes": {
"webshoppingagg": "Shopping Aggregator for Web Clients"
}
},
"ReverseProxy": {
"Routes": {
"c-short": {
"ClusterId": "catalog",
"Match": {
"Path": "c/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/c" }
]
},
"c-long": {
"ClusterId": "catalog",
"Match": {
"Path": "catalog-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/catalog-api" }
]
},
"b-short": {
"ClusterId": "basket",
"Match": {
"Path": "b/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/b" }
]
},
"b-long": {
"ClusterId": "basket",
"Match": {
"Path": "basket-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/basket-api" }
]
},
"o-short": {
"ClusterId": "orders",
"Match": {
"Path": "o/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/o" }
]
},
"o-long": {
"ClusterId": "orders",
"Match": {
"Path": "ordering-api/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/ordering-api" }
]
},
"h-long": {
"ClusterId": "signalr",
"Match": {
"Path": "hub/notificationhub/{**catch-all}"
}
} }
}, },
"Console": { "Clusters": {
"LogLevel": { "basket": {
"Default": "Warning" "Destinations": {
"destination0": {
"Address": "http://localhost:5221"
}
}
},
"catalog": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5222"
}
}
},
"orders": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5224"
}
}
},
"signalr": {
"Destinations": {
"destination0": {
"Address": "http://localhost:5225"
}
}
} }
} }
} },
"Urls": {
"Basket": "http://localhost:5221",
"Catalog": "http://localhost:5222",
"Orders": "http://localhost:5224",
"Identity": "http://localhost:5223",
"Signalr": "http://localhost:5225",
"GrpcBasket": "http://localhost:6221",
"GrpcCatalog": "http://localhost:6222",
"GrpcOrdering": "http://localhost:6224"
},
"CatalogUrlHC": "http://localhost:5222/hc",
"OrderingUrlHC": "http://localhost:5224/hc",
"BasketUrlHC": "http://localhost:5221/hc",
"IdentityUrlHC": "http://localhost:5223/hc"
} }

View File

@ -1,11 +0,0 @@
{
"urls": {
"basket": "http://localhost:55103",
"catalog": "http://localhost:55101",
"orders": "http://localhost:55102",
"identity": "http://localhost:55105",
"grpcBasket": "http://localhost:5580",
"grpcCatalog": "http://localhost:81",
"grpcOrdering": "http://localhost:5581"
}
}

View File

@ -17,7 +17,7 @@ namespace EventBus.Tests
public void After_One_Event_Subscription_Should_Contain_The_Event() public void After_One_Event_Subscription_Should_Contain_The_Event()
{ {
var manager = new InMemoryEventBusSubscriptionsManager(); var manager = new InMemoryEventBusSubscriptionsManager();
manager.AddSubscription<TestIntegrationEvent,TestIntegrationEventHandler>(); manager.AddSubscription<TestIntegrationEvent, TestIntegrationEventHandler>();
Assert.True(manager.HasSubscriptionsForEvent<TestIntegrationEvent>()); Assert.True(manager.HasSubscriptionsForEvent<TestIntegrationEvent>());
} }

View File

@ -12,9 +12,10 @@ namespace EventBus.Tests
Handled = false; Handled = false;
} }
public async Task Handle(TestIntegrationEvent @event) public Task Handle(TestIntegrationEvent @event)
{ {
Handled = true; Handled = true;
return Task.CompletedTask;
} }
} }
} }

View File

@ -12,9 +12,10 @@ namespace EventBus.Tests
Handled = false; Handled = false;
} }
public async Task Handle(TestIntegrationEvent @event) public Task Handle(TestIntegrationEvent @event)
{ {
Handled = true; Handled = true;
return Task.CompletedTask;
} }
} }
} }

View File

@ -8,12 +8,6 @@ public interface IEventBus
where T : IntegrationEvent where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>; where TH : IIntegrationEventHandler<T>;
void SubscribeDynamic<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
void UnsubscribeDynamic<TH>(string eventName)
where TH : IDynamicIntegrationEventHandler;
void Unsubscribe<T, TH>() void Unsubscribe<T, TH>()
where TH : IIntegrationEventHandler<T> where TH : IIntegrationEventHandler<T>
where T : IntegrationEvent; where T : IntegrationEvent;

View File

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBus</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBus</RootNamespace>
</PropertyGroup> </PropertyGroup>

View File

@ -1,7 +1,7 @@
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
public record IntegrationEvent public record IntegrationEvent
{ {
public IntegrationEvent() public IntegrationEvent()
{ {
Id = Guid.NewGuid(); Id = Guid.NewGuid();

View File

@ -1,8 +1,4 @@
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using System.Text.Json.Serialization;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; global using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text.Json.Serialization;
global using System.Threading.Tasks;
global using System;

View File

@ -59,7 +59,7 @@ public class DefaultRabbitMQPersistentConnection
.Or<BrokerUnreachableException>() .Or<BrokerUnreachableException>()
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
{ {
_logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message); _logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s", $"{time.TotalSeconds:n1}");
} }
); );
@ -81,7 +81,7 @@ public class DefaultRabbitMQPersistentConnection
} }
else else
{ {
_logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); _logger.LogCritical("Fatal error: RabbitMQ connections could not be created and opened");
return false; return false;
} }

View File

@ -4,7 +4,9 @@ using Microsoft.Extensions.DependencyInjection;
public class EventBusRabbitMQ : IEventBus, IDisposable public class EventBusRabbitMQ : IEventBus, IDisposable
{ {
const string BROKER_NAME = "eshop_event_bus"; const string BROKER_NAME = "eshop_event_bus";
const string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };
private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
private readonly IRabbitMQPersistentConnection _persistentConnection; private readonly IRabbitMQPersistentConnection _persistentConnection;
private readonly ILogger<EventBusRabbitMQ> _logger; private readonly ILogger<EventBusRabbitMQ> _logger;
@ -58,7 +60,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
.Or<SocketException>() .Or<SocketException>()
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => .WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
{ {
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message); _logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s", @event.Id, $"{time.TotalSeconds:n1}");
}); });
var eventName = @event.GetType().Name; var eventName = @event.GetType().Name;
@ -70,17 +72,14 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), new JsonSerializerOptions var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), s_indentedOptions);
{
WriteIndented = true
});
policy.Execute(() => policy.Execute(() =>
{ {
var properties = channel.CreateBasicProperties(); var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent properties.DeliveryMode = 2; // persistent
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id); _logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id);
channel.BasicPublish( channel.BasicPublish(
exchange: BROKER_NAME, exchange: BROKER_NAME,
@ -123,7 +122,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
{ {
_persistentConnection.TryConnect(); _persistentConnection.TryConnect();
} }
_consumerChannel.QueueBind(queue: _queueName, _consumerChannel.QueueBind(queue: _queueName,
exchange: BROKER_NAME, exchange: BROKER_NAME,
routingKey: eventName); routingKey: eventName);
@ -194,7 +193,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message); _logger.LogWarning(ex, "Error Processing message \"{Message}\"", message);
} }
// Even on exception we take the message off the queue. // Even on exception we take the message off the queue.
@ -257,7 +256,7 @@ public class EventBusRabbitMQ : IEventBus, IDisposable
var handler = scope.ServiceProvider.GetService(subscription.HandlerType); var handler = scope.ServiceProvider.GetService(subscription.HandlerType);
if (handler == null) continue; if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName); var eventType = _subsManager.GetEventTypeByName(eventName);
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); var integrationEvent = JsonSerializer.Deserialize(message, eventType, s_caseInsensitiveOptions);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await Task.Yield(); await Task.Yield();

View File

@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" />
<PackageReference Include="Microsoft.CSharp" /> <PackageReference Include="Microsoft.CSharp" />
<PackageReference Include="Microsoft.Extensions.Logging" /> <PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Polly" /> <PackageReference Include="Polly" />

View File

@ -1,17 +1,13 @@
global using Microsoft.Extensions.Logging; global using System.Net.Sockets;
global using System.Text;
global using System.Text.Json;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions;
global using Microsoft.Extensions.Logging;
global using Polly; global using Polly;
global using Polly.Retry; global using Polly.Retry;
global using RabbitMQ.Client; global using RabbitMQ.Client;
global using RabbitMQ.Client.Events; global using RabbitMQ.Client.Events;
global using RabbitMQ.Client.Exceptions; global using RabbitMQ.Client.Exceptions;
global using System;
global using System.IO;
global using System.Net.Sockets;
global using Autofac;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Extensions;
global using System.Text;
global using System.Threading.Tasks;
global using System.Text.Json;

View File

@ -27,7 +27,7 @@ public class DefaultServiceBusPersisterConnection : IServiceBusPersisterConnecti
} }
} }
public ServiceBusAdministrationClient AdministrationClient => public ServiceBusAdministrationClient AdministrationClient =>
_subscriptionClient; _subscriptionClient;
public ServiceBusClient CreateModel() public ServiceBusClient CreateModel()

View File

@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
@ -12,7 +12,6 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable
private readonly string _subscriptionName; private readonly string _subscriptionName;
private readonly ServiceBusSender _sender; private readonly ServiceBusSender _sender;
private readonly ServiceBusProcessor _processor; private readonly ServiceBusProcessor _processor;
private readonly string AUTOFAC_SCOPE_NAME = "eshop_event_bus";
private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent"; private const string INTEGRATION_EVENT_SUFFIX = "IntegrationEvent";
public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection, public EventBusServiceBus(IServiceBusPersisterConnection serviceBusPersisterConnection,
@ -141,7 +140,7 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable
var ex = args.Exception; var ex = args.Exception;
var context = args.ErrorSource; var context = args.ErrorSource;
_logger.LogError(ex, "ERROR handling message: {ExceptionMessage} - Context: {@ExceptionContext}", ex.Message, context); _logger.LogError(ex, "Error handling message - Context: {@ExceptionContext}", context);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -198,4 +197,4 @@ public class EventBusServiceBus : IEventBus, IAsyncDisposable
_subsManager.Clear(); _subsManager.Clear();
await _processor.CloseAsync(); await _processor.CloseAsync();
} }
} }

View File

@ -2,11 +2,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" />
<PackageReference Include="Azure.Messaging.ServiceBus" /> <PackageReference Include="Azure.Messaging.ServiceBus" />
<PackageReference Include="Microsoft.CSharp" /> <PackageReference Include="Microsoft.CSharp" />
<PackageReference Include="Microsoft.Extensions.Logging" /> <PackageReference Include="Microsoft.Extensions.Logging" />

View File

@ -1,14 +1,11 @@
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using System.Text;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using System.Threading.Tasks;
global using System;
global using Autofac;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
global using Microsoft.Extensions.Logging;
global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using Azure.Messaging.ServiceBus; global using Azure.Messaging.ServiceBus;
global using Azure.Messaging.ServiceBus.Administration; global using Azure.Messaging.ServiceBus.Administration;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using Microsoft.Extensions.Logging;

View File

@ -1,12 +1,8 @@
global using Microsoft.EntityFrameworkCore; global using System.ComponentModel.DataAnnotations.Schema;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using System;
global using System.Text.Json;
global using System.ComponentModel.DataAnnotations.Schema;
global using System.Linq;
global using System.Threading.Tasks;
global using Microsoft.EntityFrameworkCore.Storage;
global using System.Collections.Generic;
global using System.Data.Common; global using System.Data.Common;
global using System.Reflection; global using System.Reflection;
global using System.Text.Json;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Metadata.Builders;
global using Microsoft.EntityFrameworkCore.Storage;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;

View File

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF</RootNamespace> <RootNamespace>Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF</RootNamespace>
</PropertyGroup> </PropertyGroup>

View File

@ -2,16 +2,16 @@
public class IntegrationEventLogEntry public class IntegrationEventLogEntry
{ {
private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };
private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };
private IntegrationEventLogEntry() { } private IntegrationEventLogEntry() { }
public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId) public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId)
{ {
EventId = @event.Id; EventId = @event.Id;
CreationTime = @event.CreationDate; CreationTime = @event.CreationDate;
EventTypeName = @event.GetType().FullName; EventTypeName = @event.GetType().FullName;
Content = JsonSerializer.Serialize(@event, @event.GetType(), new JsonSerializerOptions Content = JsonSerializer.Serialize(@event, @event.GetType(), s_indentedOptions);
{
WriteIndented = true
});
State = EventStateEnum.NotPublished; State = EventStateEnum.NotPublished;
TimesSent = 0; TimesSent = 0;
TransactionId = transactionId.ToString(); TransactionId = transactionId.ToString();
@ -29,8 +29,8 @@ public class IntegrationEventLogEntry
public string TransactionId { get; private set; } public string TransactionId { get; private set; }
public IntegrationEventLogEntry DeserializeJsonContent(Type type) public IntegrationEventLogEntry DeserializeJsonContent(Type type)
{ {
IntegrationEvent = JsonSerializer.Deserialize(Content, type, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) as IntegrationEvent; IntegrationEvent = JsonSerializer.Deserialize(Content, type, s_caseInsensitiveOptions) as IntegrationEvent;
return this; return this;
} }
} }

View File

@ -2,23 +2,15 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.NETCore.Platforms" />
<PackageReference Include="Polly" /> <PackageReference Include="Polly" />
<PackageReference Include="System.Data.SqlClient" /> <PackageReference Include="System.Data.SqlClient" />
</ItemGroup> </ItemGroup>

View File

@ -1,77 +1,75 @@
using Microsoft.EntityFrameworkCore; using System.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Polly; using Polly;
using System;
using System.Data.SqlClient;
namespace Microsoft.AspNetCore.Hosting namespace Microsoft.AspNetCore.Hosting;
public static class IWebHostExtensions
{ {
public static class IWebHostExtensions public static bool IsInKubernetes(this IServiceProvider services)
{ {
public static bool IsInKubernetes(this IWebHost webHost) var cfg = services.GetService<IConfiguration>();
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
return orchestratorType?.ToUpper() == "K8S";
}
public static IServiceProvider MigrateDbContext<TContext>(this IServiceProvider services, Action<TContext, IServiceProvider> seeder) where TContext : DbContext
{
var underK8s = services.IsInKubernetes();
using var scope = services.CreateScope();
var scopeServices = scope.ServiceProvider;
var logger = scopeServices.GetRequiredService<ILogger<TContext>>();
var context = scopeServices.GetService<TContext>();
try
{ {
var cfg = webHost.Services.GetService<IConfiguration>(); logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);
var orchestratorType = cfg.GetValue<string>("OrchestratorType");
return orchestratorType?.ToUpper() == "K8S";
}
public static IWebHost MigrateDbContext<TContext>(this IWebHost webHost, Action<TContext, IServiceProvider> seeder) where TContext : DbContext if (underK8s)
{
var underK8s = webHost.IsInKubernetes();
using var scope = webHost.Services.CreateScope();
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<TContext>>();
var context = services.GetService<TContext>();
try
{ {
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); InvokeSeeder(seeder, context, scopeServices);
if (underK8s)
{
InvokeSeeder(seeder, context, services);
}
else
{
var retries = 10;
var retry = Policy.Handle<SqlException>()
.WaitAndRetry(
retryCount: retries,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Exception {ExceptionType} with message {Message} detected on attempt {retry} of {retries}", nameof(TContext), exception.GetType().Name, exception.Message, retry, retries);
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, services));
}
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
} }
catch (Exception ex) else
{ {
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); var retries = 10;
if (underK8s) var retry = Policy.Handle<SqlException>()
{ .WaitAndRetry(
throw; // Rethrow under k8s because we rely on k8s to re-run the pod retryCount: retries,
} sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogWarning(exception, "[{prefix}] Error migrating database (attempt {retry} of {retries})", nameof(TContext), retry, retries);
});
//if the sql server container is not created on run docker compose this
//migration can't fail for network related exception. The retry options for DbContext only
//apply to transient exceptions
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
retry.Execute(() => InvokeSeeder(seeder, context, scopeServices));
} }
return webHost; logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
if (underK8s)
{
throw; // Rethrow under k8s because we rely on k8s to re-run the pod
}
} }
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) return services;
where TContext : DbContext }
{
context.Database.Migrate(); private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services)
seeder(context, services); where TContext : DbContext
} {
context.Database.Migrate();
seeder(context, services);
} }
} }

View File

@ -3,7 +3,6 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" /> <PackageVersion Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
<PackageVersion Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" /> <PackageVersion Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" />
@ -14,8 +13,6 @@
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" /> <PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="6.0.5" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="6.0.5" /> <PackageVersion Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="6.0.5" />
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" /> <PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" />
<PackageVersion Include="Autofac" Version="6.5.0" />
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" /> <PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageVersion Include="Azure.Identity" Version="1.8.2" /> <PackageVersion Include="Azure.Identity" Version="1.8.2" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" /> <PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.12.0" />
@ -29,6 +26,7 @@
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" /> <PackageVersion Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageVersion Include="Google.Protobuf" Version="3.22.0" /> <PackageVersion Include="Google.Protobuf" Version="3.22.0" />
<PackageVersion Include="Grpc.AspNetCore.Server" Version="2.51.0" /> <PackageVersion Include="Grpc.AspNetCore.Server" Version="2.51.0" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.51.0" />
<PackageVersion Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.51.0" /> <PackageVersion Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.51.0" />
<PackageVersion Include="Grpc.Core" Version="2.46.6" /> <PackageVersion Include="Grpc.Core" Version="2.46.6" />
<PackageVersion Include="Grpc.Net.Client" Version="2.51.0" /> <PackageVersion Include="Grpc.Net.Client" Version="2.51.0" />
@ -51,6 +49,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="7.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.3" />
@ -75,19 +74,13 @@
<PackageVersion Include="Microsoft.Extensions.Logging.AzureAppServices" Version="7.0.3" /> <PackageVersion Include="Microsoft.Extensions.Logging.AzureAppServices" Version="7.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.NETCore.Platforms" Version="7.0.0" /> <PackageVersion Include="Microsoft.NETCore.Platforms" Version="7.0.0" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" /> <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.4" /> <PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.4" />
<PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" /> <PackageVersion Include="Microsoft.Web.LibraryManager.Build" Version="2.1.175" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
<PackageVersion Include="Polly" Version="7.2.3" /> <PackageVersion Include="Polly" Version="7.2.3" />
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0" /> <PackageVersion Include="RabbitMQ.Client" Version="6.4.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="2.2.1-dev-00787" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.5.0-dev-00359" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.1-dev-00896" />
<PackageVersion Include="Serilog.Sinks.Http" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Seq" Version="5.2.3-dev-00260" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
<PackageVersion Include="System.Data.SqlClient" Version="4.8.5" /> <PackageVersion Include="System.Data.SqlClient" Version="4.8.5" />
@ -95,5 +88,6 @@
<PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageVersion Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageVersion Include="xunit" Version="2.4.2" /> <PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,28 +0,0 @@
(function ($, swaggerUi) {
$(function () {
var settings = {
authority: 'https://localhost:5105',
client_id: 'js',
popup_redirect_uri: window.location.protocol
+ '//'
+ window.location.host
+ '/tokenclient/popup.html',
response_type: 'id_token token',
scope: 'openid profile basket',
filter_protocol_claims: true
},
manager = new OidcTokenManager(settings),
$inputApiKey = $('#input_apiKey');
$inputApiKey.on('dblclick', function () {
manager.openPopupForTokenAsync()
.then(function () {
$inputApiKey.val(manager.access_token).change();
}, function (error) {
console.error(error);
});
});
});
})(jQuery, window.swaggerUi);

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript" src="oidc-token-manager.min.js"></script>
<script type="text/javascript">
new OidcTokenManager().processTokenPopup();
</script>
</body>
</html>

View File

@ -1,25 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server;
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
if (isAuthorized && !allowAnonymous)
{
operation.Parameters ??= new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "Authorization",
In = ParameterLocation.Header,
Description = "access token",
Required = true
});
}
}
}

View File

@ -1,59 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> <ImplicitUsings>enable</ImplicitUsings>
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> <DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> <UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> </PropertyGroup>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Update="web.config"> <PackageReference Include="Grpc.AspNetCore" />
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <PackageReference Include="AspNetCore.HealthChecks.Redis" />
</Content> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> <Protobuf Include="Proto\basket.proto" GrpcServices="Server" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" /> </ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore.Server" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Http" />
<PackageReference Include="Serilog.Sinks.Seq" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> <ProjectReference Include="..\..\Services.Common\Services.Common.csproj" />
<Content Include="@(Protobuf)" /> </ItemGroup>
<None Remove="@(Protobuf)" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> <InternalsVisibleTo Include="Basket.FunctionalTests" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> </ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -1,7 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API;
public class BasketSettings
{
public string ConnectionString { get; set; }
}

View File

@ -23,7 +23,6 @@ public class BasketController : ControllerBase
} }
[HttpGet("{id}")] [HttpGet("{id}")]
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
public async Task<ActionResult<CustomerBasket>> GetBasketByIdAsync(string id) public async Task<ActionResult<CustomerBasket>> GetBasketByIdAsync(string id)
{ {
var basket = await _repository.GetBasketAsync(id); var basket = await _repository.GetBasketAsync(id);
@ -32,7 +31,6 @@ public class BasketController : ControllerBase
} }
[HttpPost] [HttpPost]
[ProducesResponseType(typeof(CustomerBasket), (int)HttpStatusCode.OK)]
public async Task<ActionResult<CustomerBasket>> UpdateBasketAsync([FromBody] CustomerBasket value) public async Task<ActionResult<CustomerBasket>> UpdateBasketAsync([FromBody] CustomerBasket value)
{ {
return Ok(await _repository.UpdateBasketAsync(value)); return Ok(await _repository.UpdateBasketAsync(value));
@ -40,8 +38,8 @@ public class BasketController : ControllerBase
[Route("checkout")] [Route("checkout")]
[HttpPost] [HttpPost]
[ProducesResponseType((int)HttpStatusCode.Accepted)] [ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId) public async Task<ActionResult> CheckoutAsync([FromBody] BasketCheckout basketCheckout, [FromHeader(Name = "x-requestid")] string requestId)
{ {
var userId = _identityService.GetUserIdentity(); var userId = _identityService.GetUserIdentity();
@ -56,7 +54,7 @@ public class BasketController : ControllerBase
return BadRequest(); return BadRequest();
} }
var userName = this.HttpContext.User.FindFirst(x => x.Type == ClaimTypes.Name).Value; var userName = User.FindFirst(x => x.Type == ClaimTypes.Name).Value;
var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street, var eventMessage = new UserCheckoutAcceptedIntegrationEvent(userId, userName, basketCheckout.City, basketCheckout.Street,
basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName, basketCheckout.State, basketCheckout.Country, basketCheckout.ZipCode, basketCheckout.CardNumber, basketCheckout.CardHolderName,
@ -71,7 +69,7 @@ public class BasketController : ControllerBase
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "ERROR Publishing integration event: {IntegrationEventId} from {AppName}", eventMessage.Id, Program.AppName); _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId}", eventMessage.Id);
throw; throw;
} }
@ -81,7 +79,7 @@ public class BasketController : ControllerBase
// DELETE api/values/5 // DELETE api/values/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task DeleteBasketByIdAsync(string id) public async Task DeleteBasketByIdAsync(string id)
{ {
await _repository.DeleteBasketAsync(id); await _repository.DeleteBasketAsync(id);

View File

@ -1,11 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return new RedirectResult("~/swagger");
}
}

View File

@ -1,37 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API;
public static class CustomExtensionMethods
{
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var hcBuilder = services.AddHealthChecks();
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy());
hcBuilder
.AddRedis(
configuration["ConnectionString"],
name: "redis-check",
tags: new string[] { "redis" });
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
hcBuilder
.AddAzureServiceBusTopic(
configuration["EventBusConnection"],
topicName: "eshop_event_bus",
name: "basket-servicebus-check",
tags: new string[] { "servicebus" });
}
else
{
hcBuilder
.AddRabbitMQ(
$"amqp://{configuration["EventBusConnection"]}",
name: "basket-rabbitmqbus-check",
tags: new string[] { "rabbitmqbus" });
}
return services;
}
}

View File

@ -32,6 +32,8 @@ COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj"
COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj"
COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj"
COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj"
COPY "Services/Contact/Contact.API/Contact.API.csproj" "Services/Contact/Contact.API/Contact.API.csproj"
COPY "Services/Services.Common/Services.Common.csproj" "Services/Services.Common/Services.Common.csproj"
COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj"
COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj"
COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj"

View File

@ -0,0 +1,20 @@
public static class Extensions
{
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddRedis(_ => configuration.GetRequiredConnectionString("redis"), "redis", tags: new[] { "ready", "liveness" });
return services;
}
public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
{
return services.AddSingleton(sp =>
{
var redisConfig = ConfigurationOptions.Parse(configuration.GetRequiredConnectionString("redis"), true);
return ConnectionMultiplexer.Connect(redisConfig);
});
}
}

View File

@ -1,59 +1,19 @@
global using Autofac.Extensions.DependencyInjection; global using System.ComponentModel.DataAnnotations;
global using Autofac; global using System.Security.Claims;
global using Azure.Core; global using System.Text.Json;
global using Azure.Identity;
global using Basket.API.Infrastructure.ActionResults;
global using Basket.API.Infrastructure.Exceptions;
global using Basket.API.Infrastructure.Filters;
global using Basket.API.Infrastructure.Middlewares;
global using Basket.API.IntegrationEvents.EventHandling; global using Basket.API.IntegrationEvents.EventHandling;
global using Basket.API.IntegrationEvents.Events; global using Basket.API.IntegrationEvents.Events;
global using Basket.API.Model; global using Basket.API.Model;
global using Basket.API.Repositories;
global using Grpc.Core; global using Grpc.Core;
global using GrpcBasket; global using GrpcBasket;
global using HealthChecks.UI.Client;
global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Http.Features;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc.Authorization;
global using Microsoft.AspNetCore.Mvc.Filters;
global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Server.Kestrel.Core;
global using Microsoft.AspNetCore;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers;
global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories;
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling;
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events;
global using Microsoft.eShopOnContainers.Services.Basket.API.Model; global using Microsoft.eShopOnContainers.Services.Basket.API.Model;
global using Microsoft.eShopOnContainers.Services.Basket.API.Services; global using Microsoft.eShopOnContainers.Services.Basket.API.Services;
global using Microsoft.eShopOnContainers.Services.Basket.API; global using Services.Common;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Microsoft.OpenApi.Models;
global using RabbitMQ.Client;
global using Serilog.Context;
global using Serilog;
global using StackExchange.Redis; global using StackExchange.Redis;
global using Swashbuckle.AspNetCore.SwaggerGen;
global using System.Collections.Generic;
global using System.ComponentModel.DataAnnotations;
global using System.IdentityModel.Tokens.Jwt;
global using System.IO;
global using System.Linq;
global using System.Net;
global using System.Security.Claims;
global using System.Text.Json;
global using System.Threading.Tasks;
global using System;

View File

@ -1,11 +0,0 @@
namespace Basket.API.Infrastructure.ActionResults;
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}

View File

@ -1,16 +0,0 @@
namespace Basket.API.Infrastructure.Exceptions;
public class BasketDomainException : Exception
{
public BasketDomainException()
{ }
public BasketDomainException(string message)
: base(message)
{ }
public BasketDomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}

View File

@ -1,18 +0,0 @@
namespace Basket.API.Infrastructure.Middlewares;
public static class FailingMiddlewareAppBuilderExtensions
{
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder)
{
return UseFailingMiddleware(builder, null);
}
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action)
{
var options = new FailingOptions();
action?.Invoke(options);
builder.UseMiddleware<FailingMiddleware>(options);
return builder;
}
}

View File

@ -1,47 +0,0 @@
namespace Basket.API.Infrastructure.Filters;
public partial class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment env;
private readonly ILogger<HttpGlobalExceptionFilter> logger;
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this.env = env;
this.logger = logger;
}
public void OnException(ExceptionContext context)
{
logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
if (context.Exception.GetType() == typeof(BasketDomainException))
{
var json = new JsonErrorResponse
{
Messages = new[] { context.Exception.Message }
};
context.Result = new BadRequestObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
else
{
var json = new JsonErrorResponse
{
Messages = new[] { "An error occurred. Try it again." }
};
if (env.IsDevelopment())
{
json.DeveloperMessage = context.Exception;
}
context.Result = new InternalServerErrorObjectResult(json);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
context.ExceptionHandled = true;
}
}

View File

@ -1,9 +0,0 @@
namespace Basket.API.Infrastructure.Filters;
public class JsonErrorResponse
{
public string[] Messages { get; set; }
public object DeveloperMessage { get; set; }
}

View File

@ -1,26 +0,0 @@
namespace Basket.API.Infrastructure.Filters;
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
{
return;
}
var validationErrors = context.ModelState
.Keys
.SelectMany(k => context.ModelState[k].Errors)
.Select(e => e.ErrorMessage)
.ToArray();
var json = new JsonErrorResponse
{
Messages = validationErrors
};
context.Result = new BadRequestObjectResult(json);
}
}

View File

@ -1,29 +0,0 @@
namespace Basket.API.Infrastructure.Filters;
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[ oAuthScheme ] = new [] { "basketapi" }
}
};
}
}

View File

@ -1,90 +0,0 @@
namespace Basket.API.Infrastructure.Middlewares;
using Microsoft.Extensions.Logging;
public class FailingMiddleware
{
private readonly RequestDelegate _next;
private bool _mustFail;
private readonly FailingOptions _options;
private readonly ILogger _logger;
public FailingMiddleware(RequestDelegate next, ILogger<FailingMiddleware> logger, FailingOptions options)
{
_next = next;
_options = options;
_mustFail = false;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path;
if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase))
{
await ProcessConfigRequest(context);
return;
}
if (MustFail(context))
{
_logger.LogInformation("Response for path {Path} will fail.", path);
context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("Failed due to FailingMiddleware enabled.");
}
else
{
await _next.Invoke(context);
}
}
private async Task ProcessConfigRequest(HttpContext context)
{
var enable = context.Request.Query.Keys.Any(k => k == "enable");
var disable = context.Request.Query.Keys.Any(k => k == "disable");
if (enable && disable)
{
throw new ArgumentException("Must use enable or disable querystring values, but not both");
}
if (disable)
{
_mustFail = false;
await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed.");
return;
}
if (enable)
{
_mustFail = true;
await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500");
return;
}
// If reach here, that means that no valid parameter has been passed. Just output status
await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled"));
return;
}
private async Task SendOkResponse(HttpContext context, string message)
{
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(message);
}
private bool MustFail(HttpContext context)
{
var rpath = context.Request.Path.Value;
if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase)))
{
return false;
}
return _mustFail &&
(_options.EndpointPaths.Any(x => x == rpath)
|| _options.EndpointPaths.Count == 0);
}
}

View File

@ -1,10 +0,0 @@
namespace Basket.API.Infrastructure.Middlewares;
public class FailingOptions
{
public string ConfigPath = "/Failing";
public List<string> EndpointPaths { get; set; } = new List<string>();
public List<string> NotFilteredPaths { get; set; } = new List<string>();
}

View File

@ -1,20 +0,0 @@
namespace Basket.API.Infrastructure.Middlewares;
public class FailingStartupFilter : IStartupFilter
{
private readonly Action<FailingOptions> _options;
public FailingStartupFilter(Action<FailingOptions> optionsAction)
{
_options = optionsAction;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseFailingMiddleware(_options);
next(app);
};
}
}

View File

@ -1,14 +0,0 @@
namespace Basket.API.Infrastructure.Middlewares;
public static class WebHostBuildertExtensions
{
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options));
});
return builder;
}
}

View File

@ -15,9 +15,9 @@ public class OrderStartedIntegrationEventHandler : IIntegrationEventHandler<Orde
public async Task Handle(OrderStartedIntegrationEvent @event) public async Task Handle(OrderStartedIntegrationEvent @event)
{ {
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) }))
{ {
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event);
await _repository.DeleteBasketAsync(@event.UserId.ToString()); await _repository.DeleteBasketAsync(@event.UserId.ToString());
} }

View File

@ -15,9 +15,9 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl
public async Task Handle(ProductPriceChangedIntegrationEvent @event) public async Task Handle(ProductPriceChangedIntegrationEvent @event)
{ {
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-{Program.AppName}")) using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new ("IntegrationEventContext", @event.Id) }))
{ {
_logger.LogInformation("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, Program.AppName, @event); _logger.LogInformation("Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})", @event.Id, @event);
var userIds = _repository.GetUsers(); var userIds = _repository.GetUsers();
@ -36,7 +36,7 @@ public class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandl
if (itemsToUpdate != null) if (itemsToUpdate != null)
{ {
_logger.LogInformation("----- ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate); _logger.LogInformation("ProductPriceChangedIntegrationEventHandler - Updating items in basket for user: {BuyerId} ({@Items})", basket.BuyerId, itemsToUpdate);
foreach (var item in itemsToUpdate) foreach (var item in itemsToUpdate)
{ {

View File

@ -1,284 +1,30 @@
using Autofac.Core; var builder = WebApplication.CreateBuilder(args);
using Microsoft.Azure.Amqp.Framing;
using Microsoft.Extensions.Configuration;
var appName = "Basket.API"; builder.AddServiceDefaults();
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory()
});
if (builder.Configuration.GetValue<bool>("UseVault", false)) {
TokenCredential credential = new ClientSecretCredential(
builder.Configuration["Vault:TenantId"],
builder.Configuration["Vault:ClientId"],
builder.Configuration["Vault:ClientSecret"]);
builder.Configuration.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential);
}
builder.Services.AddGrpc(options => { builder.Services.AddGrpc();
options.EnableDetailedErrors = true; builder.Services.AddControllers();
}); builder.Services.AddProblemDetails();
builder.Services.AddApplicationInsightsTelemetry(builder.Configuration);
builder.Services.AddApplicationInsightsKubernetesEnricher();
builder.Services.AddControllers(options => {
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
options.Filters.Add(typeof(ValidateModelStateFilter));
}) // Added for functional tests builder.Services.AddHealthChecks(builder.Configuration);
.AddApplicationPart(typeof(BasketController).Assembly) builder.Services.AddRedis(builder.Configuration);
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
builder.Services.AddSwaggerGen(options => {
options.SwaggerDoc("v1", new OpenApiInfo {
Title = "eShopOnContainers - Basket HTTP API",
Version = "v1",
Description = "The Basket Service HTTP API"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { builder.Services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
Type = SecuritySchemeType.OAuth2, builder.Services.AddTransient<OrderStartedIntegrationEventHandler>();
Flows = new OpenApiOAuthFlows() {
Implicit = new OpenApiOAuthFlow() {
AuthorizationUrl = new Uri($"{builder.Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
TokenUrl = new Uri($"{builder.Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>() { { "basket", "Basket API" } }
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = builder.Configuration.GetValue<string>("IdentityUrl");
builder.Services.AddAuthentication("Bearer").AddJwtBearer(options => {
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "basket";
options.TokenValidationParameters.ValidateAudience = false;
});
builder.Services.AddAuthorization(options => {
options.AddPolicy("ApiScope", policy => {
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "basket");
});
});
builder.Services.AddCustomHealthCheck(builder.Configuration);
builder.Services.Configure<BasketSettings>(builder.Configuration);
builder.Services.AddSingleton<ConnectionMultiplexer>(sp => {
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
return ConnectionMultiplexer.Connect(configuration);
});
if (builder.Configuration.GetValue<bool>("AzureServiceBusEnabled")) {
builder.Services.AddSingleton<IServiceBusPersisterConnection>(sp => {
var serviceBusConnectionString = builder.Configuration["EventBusConnection"];
return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
});
}
else {
builder.Services.AddSingleton<IRabbitMQPersistentConnection>(sp => {
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory() {
HostName = builder.Configuration["EventBusConnection"],
DispatchConsumersAsync = true
};
if (!string.IsNullOrEmpty(builder.Configuration["EventBusUserName"])) {
factory.UserName = builder.Configuration["EventBusUserName"];
}
if (!string.IsNullOrEmpty(builder.Configuration["EventBusPassword"])) {
factory.Password = builder.Configuration["EventBusPassword"];
}
var retryCount = 5;
if (!string.IsNullOrEmpty(builder.Configuration["EventBusRetryCount"])) {
retryCount = int.Parse(builder.Configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
}
builder.Services.RegisterEventBus(builder.Configuration);
builder.Services.AddCors(options => {
options.AddPolicy("CorsPolicy",
builder => builder
.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>(); builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>();
builder.Services.AddTransient<IIdentityService, IdentityService>(); builder.Services.AddTransient<IIdentityService, IdentityService>();
builder.Services.AddOptions();
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory());
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.WebHost.UseKestrel(options => {
var ports = GetDefinedPorts(builder.Configuration);
options.Listen(IPAddress.Any, ports.httpPort, listenOptions => {
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => {
listenOptions.Protocols = HttpProtocols.Http2;
});
});
builder.WebHost.CaptureStartupErrors(false);
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration));
builder.WebHost.UseFailing(options => {
options.ConfigPath = "/Failing";
options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" });
});
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment()) { app.UseServiceDefaults();
app.UseDeveloperExceptionPage();
}
else {
app.UseExceptionHandler("/Home/Error");
}
var pathBase = app.Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase)) {
app.UsePathBase(pathBase);
}
app.UseSwagger()
.UseSwaggerUI(setup => {
setup.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Basket.API V1");
setup.OAuthClientId("basketswaggerui");
setup.OAuthAppName("Basket Swagger UI");
});
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.MapGrpcService<BasketService>(); app.MapGrpcService<BasketService>();
app.MapDefaultControllerRoute();
app.MapControllers(); app.MapControllers();
app.MapGet("/_proto/", async ctx => {
ctx.Response.ContentType = "text/plain";
using var fs = new FileStream(Path.Combine(app.Environment.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read);
using var sr = new StreamReader(fs);
while (!sr.EndOfStream) {
var line = await sr.ReadLineAsync();
if (line != "/* >>" || line != "<< */") {
await ctx.Response.WriteAsync(line);
}
}
});
app.MapHealthChecks("/hc", new HealthCheckOptions() {
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecks("/liveness", new HealthCheckOptions {
Predicate = r => r.Name.Contains("self")
});
ConfigureEventBus(app);
try {
Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName);
var eventBus = app.Services.GetRequiredService<IEventBus>();
Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
await app.RunAsync(); eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
return 0; await app.RunAsync();
}
catch (Exception ex) {
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName);
return 1;
}
finally {
Log.CloseAndFlush();
}
Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) {
var seqServerUrl = configuration["Serilog:SeqServerUrl"];
var logstashUrl = configuration["Serilog:LogstashgUrl"];
return new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.WithProperty("ApplicationContext", Program.AppName)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl)
.WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null)
.ReadFrom.Configuration(configuration)
.CreateLogger();
}
(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) {
var grpcPort = config.GetValue("GRPC_PORT", 5001);
var port = config.GetValue("PORT", 80);
return (port, grpcPort);
}
void ConfigureEventBus(IApplicationBuilder app) {
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
}
public partial class Program {
public static string Namespace = typeof(Program).Assembly.GetName().Name;
public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1);
}
public static class CustomExtensionMethods {
public static IServiceCollection RegisterEventBus(this IServiceCollection services, IConfiguration configuration) {
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) {
services.AddSingleton<IEventBus, EventBusServiceBus>(sp => {
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
string subscriptionName = configuration["SubscriptionClientName"];
return new EventBusServiceBus(serviceBusPersisterConnection, logger,
eventBusSubscriptionsManager, sp, subscriptionName);
});
}
else {
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => {
var subscriptionClientName = configuration["SubscriptionClientName"];
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) {
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount);
});
}
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
services.AddTransient<ProductPriceChangedIntegrationEventHandler>();
services.AddTransient<OrderStartedIntegrationEventHandler>();
return services;
}
}

View File

@ -1,26 +1,11 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:58017/",
"sslPort": 0
}
},
"profiles": { "profiles": {
"IIS Express": { "Basket.API": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Microsoft.eShopOnContainers.Services.Basket.API": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "http://localhost:55103/", "applicationUrl": "http://localhost:5221",
"environmentVariables": { "environmentVariables": {
"Identity__Url": "http://localhost:5223",
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
} }

View File

@ -0,0 +1,17 @@
{
"dependencies": {
"secrets1": {
"type": "secrets"
},
"rabbitmq1": {
"type": "rabbitmq",
"connectionId": "eventbus",
"dynamicId": null
},
"redis1": {
"type": "redis",
"connectionId": "ConnectionStrings:Redis",
"dynamicId": null
}
}
}

View File

@ -0,0 +1,26 @@
{
"dependencies": {
"secrets1": {
"type": "secrets.user"
},
"rabbitmq1": {
"containerPorts": "5672:5672,15672:15672",
"secretStore": "LocalSecretsFile",
"containerName": "rabbitmq",
"containerImage": "rabbitmq:3-management-alpine",
"type": "rabbitmq.container",
"connectionId": "eventbus",
"dynamicId": null
},
"redis1": {
"serviceConnectorResourceId": "",
"containerPorts": "6379:6379",
"secretStore": "LocalSecretsFile",
"containerName": "basket-redis",
"containerImage": "redis:alpine",
"type": "redis.container",
"connectionId": "ConnectionStrings:Redis",
"dynamicId": null
}
}
}

View File

@ -1,4 +1,4 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; namespace Basket.API.Repositories;
public class RedisBasketRepository : IBasketRepository public class RedisBasketRepository : IBasketRepository
{ {
@ -6,9 +6,9 @@ public class RedisBasketRepository : IBasketRepository
private readonly ConnectionMultiplexer _redis; private readonly ConnectionMultiplexer _redis;
private readonly IDatabase _database; private readonly IDatabase _database;
public RedisBasketRepository(ILoggerFactory loggerFactory, ConnectionMultiplexer redis) public RedisBasketRepository(ILogger<RedisBasketRepository> logger, ConnectionMultiplexer redis)
{ {
_logger = loggerFactory.CreateLogger<RedisBasketRepository>(); _logger = logger;
_redis = redis; _redis = redis;
_database = redis.GetDatabase(); _database = redis.GetDatabase();
} }
@ -35,15 +35,12 @@ public class RedisBasketRepository : IBasketRepository
return null; return null;
} }
return JsonSerializer.Deserialize<CustomerBasket>(data, new JsonSerializerOptions return JsonSerializer.Deserialize<CustomerBasket>(data, JsonDefaults.CaseInsensitiveOptions);
{
PropertyNameCaseInsensitive = true
});
} }
public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket) public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket)
{ {
var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket)); var created = await _database.StringSetAsync(basket.BuyerId, JsonSerializer.Serialize(basket, JsonDefaults.CaseInsensitiveOptions));
if (!created) if (!created)
{ {
@ -51,7 +48,7 @@ public class RedisBasketRepository : IBasketRepository
return null; return null;
} }
_logger.LogInformation("Basket item persisted succesfully."); _logger.LogInformation("Basket item persisted successfully.");
return await GetBasketAsync(basket.BuyerId); return await GetBasketAsync(basket.BuyerId);
} }

View File

@ -1,6 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Basket.API;
internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature
{
public IHeaderDictionary Trailers { get; set; }
}

View File

@ -1,16 +1,4 @@
{ {
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"Microsoft.eShopOnContainers": "Debug",
"System": "Warning"
}
}
},
"IdentityUrlExternal": "http://localhost:5105",
"IdentityUrl": "http://localhost:5105",
"ConnectionString": "127.0.0.1", "ConnectionString": "127.0.0.1",
"AzureServiceBusEnabled": false, "AzureServiceBusEnabled": false,
"EventBusConnection": "localhost" "EventBusConnection": "localhost"

View File

@ -1,30 +1,48 @@
{ {
"Serilog": { "Logging": {
"SeqServerUrl": null, "LogLevel": {
"LogstashgUrl": null,
"MinimumLevel": {
"Default": "Information", "Default": "Information",
"Override": { "Microsoft.AspNetCore": "Warning"
"Microsoft": "Warning",
"Microsoft.eShopOnContainers": "Information",
"System": "Warning"
}
} }
}, },
"Kestrel": { "Kestrel": {
"EndpointDefaults": { "Endpoints": {
"Protocols": "Http2" "Http": {
"Url": "http://localhost:5221"
},
"gRPC": {
"Url": "http://localhost:6221",
"Protocols": "Http2"
}
} }
}, },
"SubscriptionClientName": "Basket", "OpenApi": {
"ApplicationInsights": { "Endpoint": {
"InstrumentationKey": "" "Name": "Basket.API V1"
},
"Document": {
"Description": "The Basket Service HTTP API",
"Title": "eShopOnContainers - Basket HTTP API",
"Version": "v1"
},
"Auth": {
"ClientId": "basketswaggerui",
"AppName": "Basket Swagger UI"
}
}, },
"EventBusRetryCount": 5, "ConnectionStrings": {
"UseVault": false, "Redis": "localhost",
"Vault": { "EventBus": "localhost"
"Name": "eshop", },
"ClientId": "your-client-id", "Identity": {
"ClientSecret": "your-client-secret" "Audience": "basket",
"Url": "http://localhost:5223",
"Scopes": {
"basket": "Basket API"
}
},
"EventBus": {
"SubscriptionClientName": "Basket",
"RetryCount": 5
} }
} }

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
-->
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" hostingModel="InProcess">
<environmentVariables>
<environmentVariable name="COMPLUS_ForceENC" value="1" />
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>

View File

@ -18,6 +18,7 @@ class AutoAuthorizeMiddleware
identity.AddClaim(new Claim("sub", IDENTITY_ID)); identity.AddClaim(new Claim("sub", IDENTITY_ID));
identity.AddClaim(new Claim("unique_name", IDENTITY_ID)); identity.AddClaim(new Claim("unique_name", IDENTITY_ID));
identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID)); identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID));
identity.AddClaim(new Claim("scope", "basket"));
httpContext.User.AddIdentity(identity); httpContext.User.AddIdentity(identity);

View File

@ -1,4 +1,7 @@
namespace Basket.FunctionalTests.Base; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Hosting;
namespace Basket.FunctionalTests.Base;
public class BasketScenarioBase public class BasketScenarioBase
{ {
@ -6,18 +9,8 @@ public class BasketScenarioBase
public TestServer CreateServer() public TestServer CreateServer()
{ {
var path = Assembly.GetAssembly(typeof(BasketScenarioBase)) var factory = new BasketApplication();
.Location; return factory.Server;
var hostBuilder = new WebHostBuilder()
.UseContentRoot(Path.GetDirectoryName(path))
.ConfigureAppConfiguration(cb =>
{
cb.AddJsonFile("appsettings.json", optional: false)
.AddEnvironmentVariables();
});
return new TestServer(hostBuilder);
} }
public static class Get public static class Get
@ -26,6 +19,11 @@ public class BasketScenarioBase
{ {
return $"{ApiUrlBase}/{id}"; return $"{ApiUrlBase}/{id}";
} }
public static string GetBasketByCustomer(string customerId)
{
return $"{ApiUrlBase}/{customerId}";
}
} }
public static class Post public static class Post
@ -33,4 +31,37 @@ public class BasketScenarioBase
public static string Basket = $"{ApiUrlBase}/"; public static string Basket = $"{ApiUrlBase}/";
public static string CheckoutOrder = $"{ApiUrlBase}/checkout"; public static string CheckoutOrder = $"{ApiUrlBase}/checkout";
} }
private class BasketApplication : WebApplicationFactory<Program>
{
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IStartupFilter, AuthStartupFilter>();
});
builder.ConfigureAppConfiguration(c =>
{
var directory = Path.GetDirectoryName(typeof(BasketScenarioBase).Assembly.Location)!;
c.AddJsonFile(Path.Combine(directory, "appsettings.Basket.json"), optional: false);
});
return base.CreateHost(builder);
}
private class AuthStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseMiddleware<AutoAuthorizeMiddleware>();
next(app);
};
}
}
}
} }

View File

@ -1,13 +0,0 @@
namespace Basket.FunctionalTests.Base;
static class HttpClientExtensions
{
public static HttpClient CreateIdempotentClient(this TestServer server)
{
var client = server.CreateClient();
client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString());
return client;
}
}

View File

@ -7,11 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="appsettings.json" /> <Content Include="appsettings.Basket.json">
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>

Some files were not shown because too many files have changed in this diff Show More