Compare commits

..

No commits in common. "dev" and "release/net-5" have entirely different histories.

806 changed files with 61342 additions and 62340 deletions

View File

@ -1,132 +0,0 @@
###############################
# Core EditorConfig Options #
###############################
root = true
# All files
[*]
indent_style = space
# XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# XML config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
###############################
# .NET Coding Conventions #
###############################
[*.{cs,vb}]
# Organize usings
dotnet_sort_system_directives_first = true
# this. preferences
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_property = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_event = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
dotnet_style_readonly_field = true:suggestion
# Expression-level preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
###############################
# Naming Conventions #
###############################
# Style Definitions
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
###############################
# C# Coding Conventions #
###############################
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true:silent
csharp_style_var_when_type_is_apparent = true:silent
csharp_style_var_elsewhere = true:silent
# Expression-bodied members
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
# Null-checking preferences
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
# Expression-level preferences
csharp_prefer_braces = true:silent
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
###############################
# C# Formatting Rules #
###############################
# New line preferences
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Space preferences
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
###############################
# VB Coding Conventions #
###############################
[*.vb]
# Modifier preferences
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion

View File

@ -22,7 +22,7 @@ on:
env: env:
SERVICE: catalog-api SERVICE: catalog-api
IMAGE: catalog.api IMAGE: catalog.api
DOTNET_VERSION: 7.0.x DOTNET_VERSION: 5.0.x
PROJECT_PATH: Services/Catalog/Catalog.API PROJECT_PATH: Services/Catalog/Catalog.API
TESTS_PATH: Services/Catalog/Catalog.UnitTests TESTS_PATH: Services/Catalog/Catalog.UnitTests

View File

@ -22,7 +22,7 @@ on:
env: env:
SERVICE: ordering-api SERVICE: ordering-api
IMAGE: ordering.api IMAGE: ordering.api
DOTNET_VERSION: 7.0.x DOTNET_VERSION: 5.0.x
PROJECT_PATH: Services/Ordering/Ordering.API PROJECT_PATH: Services/Ordering/Ordering.API
TESTS_PATH: Services/Ordering/Ordering.UnitTests TESTS_PATH: Services/Ordering/Ordering.UnitTests

2
.gitignore vendored
View File

@ -281,5 +281,3 @@ pub/
src/**/app.yaml src/**/app.yaml
src/**/inf.yaml src/**/inf.yaml
.angular/
/src/Services/Identity/Identity.API/keys/*.json

View File

@ -47,7 +47,7 @@ All contributions must be submitted as a [Pull Request (PR)](https://help.github
The main branches are **`dev`** and **`master`**: The main branches are **`dev`** and **`master`**:
- **`dev`**: Contains the latest code **and it is the branch actively developed**. - **`dev`**: Contains the latest code **and it is the branch actively developed**.
**All PRs must be against `dev` branch to be considered**. This branch is developed using `.NET 7` **All PRs must be against `dev` branch to be considered**. This branch is developed using `.NET 5`
- **`main`**: Synced from time to time from **`dev`**. It contains "stable" code.This branch contains changes specific to `.NET Core 3.1` (**Keep in mind "stable" does not mean PRODUCTION-READY!**) - **`main`**: Synced from time to time from **`dev`**. It contains "stable" code.This branch contains changes specific to `.NET Core 3.1` (**Keep in mind "stable" does not mean PRODUCTION-READY!**)

View File

@ -72,9 +72,9 @@ In the future, more features will be implemented in the advanced scenario.
**NEWS / ANNOUNCEMENTS** **NEWS / ANNOUNCEMENTS**
Do you want to be up-to-date on .NET Architecture guidance and reference apps like eShopOnContainers? --> Subscribe by "WATCHING" this new GitHub repo: https://github.com/dotnet-architecture/News Do you want to be up-to-date on .NET Architecture guidance and reference apps like eShopOnContainers? --> Subscribe by "WATCHING" this new GitHub repo: https://github.com/dotnet-architecture/News
## Updated for .NET 7 ## Updated for .NET 5
eShopOnContainers is updated to .NET 7 "wave" of technologies. Not just compilation but also new recommended code in EF Core, ASP.NET Core, and other new related versions with several significant changes. eShopOnContainers is updated to .NET 5 "wave" of technologies. Not just compilation but also new recommended code in EF Core, ASP.NET Core, and other new related versions with several significant changes.
**See more details in the [Release notes](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Release-notes) wiki page**. **See more details in the [Release notes](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Release-notes) wiki page**.
@ -86,7 +86,7 @@ eShopOnContainers is updated to .NET 7 "wave" of technologies. Not just compilat
### Architecture overview ### Architecture overview
This reference application is cross-platform at the server and client-side, thanks to .NET 7 services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS, or Windows/UWP plus any browser for the client web apps. This reference application is cross-platform at the server and client-side, thanks to .NET 5 services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS, or Windows/UWP plus any browser for the client web apps.
The architecture proposes a microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using HTTP as the communication protocol between the client apps and the microservices and supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus (a light message broker, to choose between RabbitMQ or Azure Service Bus, underneath) plus other features defined at the [roadmap](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Roadmap). The architecture proposes a microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using HTTP as the communication protocol between the client apps and the microservices and supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus (a light message broker, to choose between RabbitMQ or Azure Service Bus, underneath) plus other features defined at the [roadmap](https://github.com/dotnet-architecture/eShopOnContainers/wiki/Roadmap).
![](img/eshop_logo.png) ![](img/eshop_logo.png)

View File

@ -2,12 +2,8 @@
Following are the most important branches: Following are the most important branches:
- `dev`: Contains the latest code **and it is the branch actively developed**. Note that **all PRs must be against the `dev` branch to be considered**. This branch is developed using `.NET 7` - `dev`: Contains the latest code **and it is the branch actively developed**. Note that **all PRs must be against the `dev` branch to be considered**. This branch is developed using `.NET 5`
- `release/net-6`: Contains the code changes specific to the `.NET 6` - `main`: Synced time to time from `dev`.It contains "stable" code, although not the latest one. Right now, this branch contains changes specific to `.NET Core 3.1`
- `release/net-5`: Contains the code changes specific to the `.NET 5`
- `release/net-3.1.1`: Contains the code changes specific to the `.NET 3.1`
> [!DISCLAIMER]: The `main` branch contains the old code base and will get obsolete in the future. So it's recommended to refer to different [tags](https://github.com/dotnet-architecture/eShopOnContainers/tags) to avoid any confusion.
Any other branch is considered temporary and could be deleted at any time. Do not submit any PR against them! Any other branch is considered temporary and could be deleted at any time. Do not submit any PR against them!

View File

@ -31,3 +31,5 @@ $services |% {
Write-Host "Setting ACR build $bname ($bimg)" Write-Host "Setting ACR build $bname ($bimg)"
az acr build-task create --registry $acrName --name $bname --image ${bimg}:$gitBranch --context $gitContext --branch $gitBranch --git-access-token $patToken --file $bfile az acr build-task create --registry $acrName --name $bname --image ${bimg}:$gitBranch --context $gitContext --branch $gitBranch --git-access-token $patToken --file $bfile
} }
# Basket.API

View File

@ -9,20 +9,19 @@ static_resources:
- address: - address:
socket_address: socket_address:
address: 0.0.0.0 address: 0.0.0.0
port_value: 8080 port_value: 80
filter_chains: filter_chains:
- filters: - filters:
- name: envoy.filters.network.http_connection_manager - name: envoy.http_connection_manager
typed_config: config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto
stat_prefix: ingress_http stat_prefix: ingress_http
codec_type: AUTO
route_config: route_config:
name: eshop_backend_route name: eshop_backend_route
virtual_hosts: virtual_hosts:
- name: eshop_backend - name: eshop_backend
domains: domains:
- ["*"] - "*"
routes: routes:
- name: "c-short" - name: "c-short"
match: match:
@ -78,70 +77,63 @@ static_resources:
prefix_rewrite: "/" prefix_rewrite: "/"
cluster: shoppingagg cluster: shoppingagg
http_filters: http_filters:
- name: envoy.filters.http.router - 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: clusters:
- name: shoppingagg - name: shoppingagg
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: shoppingagg - socket_address:
endpoints: address: webshoppingagg
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: webshoppingagg
port_value: 80
- name: catalog - name: catalog
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: catalog - socket_address:
endpoints: address: catalog-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: catalog-api
port_value: 80
- name: basket - name: basket
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: basket - socket_address:
endpoints: address: basket-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: basket-api
port_value: 80
- name: ordering - name: ordering
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: ordering - socket_address:
endpoints: address: ordering-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: ordering-api
port_value: 80
- name: signalr-hub - name: signalr-hub
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: signalr-hub - socket_address:
endpoints: address: ordering-signalrhub
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: ordering-signalrhub
port_value: 80

View File

@ -27,10 +27,6 @@ spec:
linkerd.io/inject: enabled linkerd.io/inject: enabled
{{- end }} {{- end }}
spec: spec:
securityContext:
runAsUser: 2000
runAsGroup: 3000
fsGroup: 2000
{{ if .Values.inf.registry -}} {{ if .Values.inf.registry -}}
imagePullSecrets: imagePullSecrets:
- name: {{ .Values.inf.registry.secretName }} - name: {{ .Values.inf.registry.secretName }}
@ -92,7 +88,7 @@ spec:
{{- end }} {{- end }}
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 80
protocol: TCP protocol: TCP
- name: admin - name: admin
containerPort: 8001 containerPort: 8001

View File

@ -39,7 +39,7 @@ spec:
- host: {{ . }} - host: {{ . }}
http: http:
paths: paths:
- path: {{ $ingressPath }}(/|$)(.*) - path: {{ $ingressPath }}
pathType: Prefix pathType: Prefix
backend: backend:
service: service:

View File

@ -4,7 +4,7 @@ pathBase: /mobileshoppingapigw
image: image:
repository: envoyproxy/envoy repository: envoyproxy/envoy
tag: v1.21.0 tag: v1.11.1
service: service:
type: ClusterIP type: ClusterIP
@ -14,9 +14,8 @@ service:
ingress: ingress:
enabled: true enabled: true
annotations: annotations:
kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: "/"
nginx.ingress.kubernetes.io/rewrite-target: /$2 ingress.kubernetes.io/rewrite-target: "/"
nginx.ingress.kubernetes.io/use-regex: "true"
tls: [] tls: []
resources: {} resources: {}

View File

@ -9,20 +9,19 @@ static_resources:
- address: - address:
socket_address: socket_address:
address: 0.0.0.0 address: 0.0.0.0
port_value: 8080 port_value: 80
filter_chains: filter_chains:
- filters: - filters:
- name: envoy.filters.network.http_connection_manager - name: envoy.http_connection_manager
typed_config: config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto
stat_prefix: ingress_http stat_prefix: ingress_http
codec_type: AUTO
route_config: route_config:
name: eshop_backend_route name: eshop_backend_route
virtual_hosts: virtual_hosts:
- name: eshop_backend - name: eshop_backend
domains: domains:
- ["*"] - "*"
routes: routes:
- name: "c-short" - name: "c-short"
match: match:
@ -81,70 +80,63 @@ static_resources:
prefix_rewrite: "/" prefix_rewrite: "/"
cluster: shoppingagg cluster: shoppingagg
http_filters: http_filters:
- name: envoy.filters.http.router - 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: clusters:
- name: shoppingagg - name: shoppingagg
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: shoppingagg - socket_address:
endpoints: address: webshoppingagg
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: webshoppingagg
port_value: 80
- name: catalog - name: catalog
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: catalog - socket_address:
endpoints: address: catalog-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: catalog-api
port_value: 80
- name: basket - name: basket
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: basket - socket_address:
endpoints: address: basket-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: basket-api
port_value: 80
- name: ordering - name: ordering
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: ordering - socket_address:
endpoints: address: ordering-api
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: ordering-api
port_value: 80
- name: signalr-hub - name: signalr-hub
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
lb_policy: round_robin lb_policy: round_robin
load_assignment: hosts:
cluster_name: signalr-hub - socket_address:
endpoints: address: ordering-signalrhub
- lb_endpoints: port_value: 80
- endpoint:
address:
socket_address:
address: ordering-signalrhub
port_value: 80

View File

@ -26,10 +26,6 @@ spec:
linkerd.io/inject: enabled linkerd.io/inject: enabled
{{- end }} {{- end }}
spec: spec:
securityContext:
runAsUser: 2000
runAsGroup: 3000
fsGroup: 2000
{{ if .Values.inf.registry -}} {{ if .Values.inf.registry -}}
imagePullSecrets: imagePullSecrets:
- name: {{ .Values.inf.registry.secretName }} - name: {{ .Values.inf.registry.secretName }}
@ -91,7 +87,7 @@ spec:
{{- end }} {{- end }}
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 80
protocol: TCP protocol: TCP
- name: admin - name: admin
containerPort: 8001 containerPort: 8001

View File

@ -38,7 +38,7 @@ spec:
- host: {{ . }} - host: {{ . }}
http: http:
paths: paths:
- path: {{ $ingressPath }}(/|$)(.*) - path: {{ $ingressPath }}
pathType: Prefix pathType: Prefix
backend: backend:
service: service:

View File

@ -4,7 +4,7 @@ pathBase: /webshoppingapigw
image: image:
repository: envoyproxy/envoy repository: envoyproxy/envoy
tag: v1.21.0 tag: v1.11.1
service: service:
type: ClusterIP type: ClusterIP
@ -14,9 +14,8 @@ service:
ingress: ingress:
enabled: true enabled: true
annotations: annotations:
kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: "/"
nginx.ingress.kubernetes.io/rewrite-target: /$2 ingress.kubernetes.io/rewrite-target: "/"
nginx.ingress.kubernetes.io/use-regex: "true"
tls: [] tls: []
resources: {} resources: {}

View File

@ -13,7 +13,7 @@ metadata:
release: {{ .Release.Name }} release: {{ .Release.Name }}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
data: data:
catalog__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.catalog.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};TrustServerCertificate={{ .Values.inf.sql.common.TrustServerCertificate }}; catalog__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.catalog.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
catalog__PicBaseUrl: {{ $protocol }}://{{ $webshoppingapigw }}/c/api/v1/catalog/items/[0]/pic/ catalog__PicBaseUrl: {{ $protocol }}://{{ $webshoppingapigw }}/c/api/v1/catalog/items/[0]/pic/
catalog__AzureStorageEnabled: "{{ .Values.inf.misc.useAzureStorage }}" catalog__AzureStorageEnabled: "{{ .Values.inf.misc.useAzureStorage }}"
all__EventBusConnection: {{ .Values.inf.eventbus.constr }} all__EventBusConnection: {{ .Values.inf.eventbus.constr }}

View File

@ -20,7 +20,7 @@ metadata:
release: {{ .Release.Name }} release: {{ .Release.Name }}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
data: data:
identity__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.identity.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};TrustServerCertificate={{ .Values.inf.sql.common.TrustServerCertificate }}; identity__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.identity.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
identity__keystore: {{ .Values.inf.redis.keystore.constr }} identity__keystore: {{ .Values.inf.redis.keystore.constr }}
all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}" all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}"
mvc_e: http://{{ $mvc_url }} mvc_e: http://{{ $mvc_url }}

View File

@ -13,7 +13,6 @@ inf:
user: sa # SQL user user: sa # SQL user
pwd: Pass@word # SQL pwd pwd: Pass@word # SQL pwd
pid: Developer pid: Developer
TrustServerCertificate: true
catalog: # inf.sql.catalog: settings for the catalog-api sql (user, pwd, db) catalog: # inf.sql.catalog: settings for the catalog-api sql (user, pwd, db)
db: CatalogDb # Catalog API SQL db name db: CatalogDb # Catalog API SQL db name
ordering: # inf.sql.ordering: settings for the ordering-api sql (user, pwd, db) ordering: # inf.sql.ordering: settings for the ordering-api sql (user, pwd, db)

View File

@ -11,7 +11,7 @@ metadata:
release: {{ .Release.Name }} release: {{ .Release.Name }}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
data: data:
ordering__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.ordering.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};TrustServerCertificate={{ .Values.inf.sql.common.TrustServerCertificate }}; ordering__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.ordering.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
urls__IdentityUrl: http://{{ .Values.app.svc.identity }} urls__IdentityUrl: http://{{ .Values.app.svc.identity }}
all__EventBusConnection: {{ .Values.inf.eventbus.constr }} all__EventBusConnection: {{ .Values.inf.eventbus.constr }}
all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}" all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}"

View File

@ -12,7 +12,7 @@ metadata:
release: {{ .Release.Name }} release: {{ .Release.Name }}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
data: data:
ordering__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.ordering.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};TrustServerCertificate={{ .Values.inf.sql.common.TrustServerCertificate }}; ordering__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.ordering.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
ordering__EnableLoadTest: "{{ .Values.inf.misc.useLoadTest }}" ordering__EnableLoadTest: "{{ .Values.inf.misc.useLoadTest }}"
all__EventBusConnection: {{ .Values.inf.eventbus.constr }} all__EventBusConnection: {{ .Values.inf.eventbus.constr }}
all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}" all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}"

View File

@ -13,7 +13,7 @@ metadata:
release: {{ .Release.Name }} release: {{ .Release.Name }}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
data: data:
webhooks__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.webhooks.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};TrustServerCertificate={{ .Values.inf.sql.common.TrustServerCertificate }}; webhooks__ConnectionString: Server={{ $sqlsrv }};Initial Catalog={{ .Values.inf.sql.webhooks.db }};User Id={{ .Values.inf.sql.common.user }};Password={{ .Values.inf.sql.common.pwd }};
urls__IdentityUrl: http://{{ $identity }} urls__IdentityUrl: http://{{ $identity }}
urls__IdentityUrlExternal: {{ $protocol }}://{{ $identity }} urls__IdentityUrlExternal: {{ $protocol }}://{{ $identity }}
all__EventBusConnection: {{ .Values.inf.eventbus.constr }} all__EventBusConnection: {{ .Values.inf.eventbus.constr }}

View File

@ -4,7 +4,7 @@ metadata:
labels: labels:
app.kubernetes.io/name: ingress-nginx app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx app.kubernetes.io/part-of: ingress-nginx
name: ingress-nginx-controller name: nginx-configuration
namespace: ingress-nginx namespace: ingress-nginx
data: data:
proxy-buffer-size: "128k" proxy-buffer-size: "128k"

View File

@ -11,16 +11,15 @@ metadata:
namespace: default namespace: default
spec: spec:
rules: rules:
- host: localhost http:
http: paths:
paths: - path: /webmvc
- path: /webmvc pathType: Prefix
pathType: Prefix backend:
backend: service:
service: name: webmvc
name: webmvc port:
port: number: 80
number: 80
--- ---
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
@ -35,13 +34,12 @@ metadata:
namespace: default namespace: default
spec: spec:
rules: rules:
- host: localhost http:
http: paths:
paths: - path: /identity
- path: /identity pathType: Prefix
pathType: Prefix backend:
backend: service:
service: name: identity
name: identity port:
port: number: 80
number: 80

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:5121/c/api/v1/catalog/items/[0]/pic/ ESHOP_STORAGE_CATALOG_URL=http://host.docker.internal:5202/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:5121/c/api/v1/catalog/items/[0]/pic/ # ESHOP_STORAGE_CATALOG_URL=http://docker.for.mac.localhost:5202/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:5121/c/api/v1/catalog/items/[0]/pic/ # ESHOP_STORAGE_CATALOG_URL=http://docker.for.linux.localhost:5202/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

@ -0,0 +1,139 @@
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

@ -0,0 +1,142 @@
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

@ -1,35 +1,38 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config; using System.Collections.Generic;
public class UrlsConfig namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config
{ {
public class CatalogOperations public class UrlsConfig
{ {
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; public class CatalogOperations
{
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}";
public static string GetItemsById(IEnumerable<int> ids) => $"/api/v1/catalog/items?ids={string.Join(',', ids)}"; public static string GetItemsById(IEnumerable<int> ids) => $"/api/v1/catalog/items?ids={string.Join(',', ids)}";
}
public class BasketOperations
{
public static string GetItemById(string id) => $"/api/v1/basket/{id}";
public static string UpdateBasket() => "/api/v1/basket";
}
public class OrdersOperations
{
public static string GetOrderDraft() => "/api/v1/orders/draft";
}
public string Basket { get; set; }
public string Catalog { get; set; }
public string Orders { get; set; }
public string GrpcBasket { get; set; }
public string GrpcCatalog { get; set; }
public string GrpcOrdering { get; set; }
} }
public class BasketOperations
{
public static string GetItemById(string id) => $"/api/v1/basket/{id}";
public static string UpdateBasket() => "/api/v1/basket";
}
public class OrdersOperations
{
public static string GetOrderDraft() => "/api/v1/orders/draft";
}
public string Basket { get; set; }
public string Catalog { get; set; }
public string Orders { get; set; }
public string GrpcBasket { get; set; }
public string GrpcCatalog { get; set; }
public string GrpcOrdering { get; set; }
} }

View File

@ -1,143 +1,156 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
[Route("api/v1/[controller]")] namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers
[Authorize]
[ApiController]
public class BasketController : ControllerBase
{ {
private readonly ICatalogService _catalog; [Route("api/v1/[controller]")]
private readonly IBasketService _basket; [Authorize]
[ApiController]
public BasketController(ICatalogService catalogService, IBasketService basketService) public class BasketController : ControllerBase
{ {
_catalog = catalogService; private readonly ICatalogService _catalog;
_basket = basketService; private readonly IBasketService _basket;
}
[HttpPost] public BasketController(ICatalogService catalogService, IBasketService basketService)
[HttpPut]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
{
if (data.Items == null || !data.Items.Any())
{ {
return BadRequest("Need to pass at least one basket line"); _catalog = catalogService;
_basket = basketService;
} }
// Retrieve the current basket [HttpPost]
var basket = await _basket.GetByIdAsync(data.BuyerId) ?? new BasketData(data.BuyerId); [HttpPut]
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); [ProducesResponseType((int)HttpStatusCode.BadRequest)]
// group by product id to avoid duplicates [ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
var itemsCalculated = data public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
.Items {
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i }) if (data.Items == null || !data.Items.Any())
.Select(groupedItem => {
return BadRequest("Need to pass at least one basket line");
}
// Retrieve the current basket
var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId);
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId));
// group by product id to avoid duplicates
var itemsCalculated = data
.Items
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i })
.Select(groupedItem =>
{
var item = groupedItem.items.First();
item.Quantity = groupedItem.items.Sum(i => i.Quantity);
return item;
});
foreach (var bitem in itemsCalculated)
{
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId);
if (catalogItem == null)
{ {
var item = groupedItem.items.First(); return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})");
item.Quantity = groupedItem.items.Sum(i => i.Quantity); }
return item;
});
foreach (var bitem in itemsCalculated) var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
{ if (itemInBasket == null)
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId);
if (catalogItem == null)
{
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})");
}
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
if (itemInBasket == null)
{
basket.Items.Add(new BasketDataItem()
{ {
Id = bitem.Id, basket.Items.Add(new BasketDataItem()
ProductId = catalogItem.Id, {
ProductName = catalogItem.Name, Id = bitem.Id,
PictureUrl = catalogItem.PictureUri, ProductId = catalogItem.Id,
UnitPrice = catalogItem.Price, ProductName = catalogItem.Name,
Quantity = bitem.Quantity PictureUrl = catalogItem.PictureUri,
}); UnitPrice = catalogItem.Price,
Quantity = bitem.Quantity
});
}
else
{
itemInBasket.Quantity = bitem.Quantity;
}
} }
else
await _basket.UpdateAsync(basket);
return basket;
}
[HttpPut]
[Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{
if (!data.Updates.Any())
{ {
itemInBasket.Quantity = bitem.Quantity; return BadRequest("No updates sent");
} }
}
await _basket.UpdateAsync(basket); // Retrieve the current basket
var currentBasket = await _basket.GetById(data.BasketId);
return basket; if (currentBasket == null)
}
[HttpPut]
[Route("items")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{
if (!data.Updates.Any())
{
return BadRequest("No updates sent");
}
// Retrieve the current basket
var currentBasket = await _basket.GetByIdAsync(data.BasketId);
if (currentBasket == null)
{
return BadRequest($"Basket with id {data.BasketId} not found.");
}
// Update with new quantities
foreach (var update in data.Updates)
{
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId);
if (basketItem == null)
{ {
return BadRequest($"Basket item with id {update.BasketItemId} not found"); return BadRequest($"Basket with id {data.BasketId} not found.");
} }
basketItem.Quantity = update.NewQty; // Update with new quantities
foreach (var update in data.Updates)
{
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId);
if (basketItem == null)
{
return BadRequest($"Basket item with id {update.BasketItemId} not found");
}
basketItem.Quantity = update.NewQty;
}
// Save the updated basket
await _basket.UpdateAsync(currentBasket);
return currentBasket;
} }
// Save the updated basket [HttpPost]
await _basket.UpdateAsync(currentBasket); [Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
return currentBasket; [ProducesResponseType((int)HttpStatusCode.OK)]
} public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
[HttpPost]
[Route("items")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
{
if (data == null || data.Quantity == 0)
{ {
return BadRequest("Invalid payload"); if (data == null || data.Quantity == 0)
{
return BadRequest("Invalid payload");
}
// Step 1: Get the item from catalog
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId);
//item.PictureUri =
// Step 2: Get current basket status
var currentBasket = (await _basket.GetById(data.BasketId)) ?? new BasketData(data.BasketId);
// Step 3: Merge current status with new product
currentBasket.Items.Add(new BasketDataItem()
{
UnitPrice = item.Price,
PictureUrl = item.PictureUri,
ProductId = item.Id,
ProductName = item.Name,
Quantity = data.Quantity,
Id = Guid.NewGuid().ToString()
});
// Step 4: Update basket
await _basket.UpdateAsync(currentBasket);
return Ok();
} }
// Step 1: Get the item from catalog
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId);
//item.PictureUri =
// Step 2: Get current basket status
var currentBasket = (await _basket.GetByIdAsync(data.BasketId)) ?? new BasketData(data.BasketId);
// Step 3: Merge current status with new product
currentBasket.Items.Add(new BasketDataItem()
{
UnitPrice = item.Price,
PictureUrl = item.PictureUri,
ProductId = item.Id,
ProductName = item.Name,
Quantity = data.Quantity,
Id = Guid.NewGuid().ToString()
});
// Step 4: Update basket
await _basket.UpdateAsync(currentBasket);
return Ok();
} }
} }

View File

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

View File

@ -1,36 +1,45 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
using System.Net;
using System.Threading.Tasks;
[Route("api/v1/[controller]")] namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Controllers
[Authorize]
[ApiController]
public class OrderController : ControllerBase
{ {
private readonly IBasketService _basketService; [Route("api/v1/[controller]")]
private readonly IOrderingService _orderingService; [Authorize]
[ApiController]
public OrderController(IBasketService basketService, IOrderingService orderingService) public class OrderController : ControllerBase
{ {
_basketService = basketService; private readonly IBasketService _basketService;
_orderingService = orderingService; private readonly IOrderingService _orderingService;
}
[Route("draft/{basketId}")] public OrderController(IBasketService basketService, IOrderingService orderingService)
[HttpGet]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{
if (string.IsNullOrEmpty(basketId))
{ {
return BadRequest("Need a valid basketid"); _basketService = basketService;
} _orderingService = orderingService;
// Get the basket data and build a order draft based on it
var basket = await _basketService.GetByIdAsync(basketId);
if (basket == null)
{
return BadRequest($"No basket found for id {basketId}");
} }
return await _orderingService.GetOrderDraftAsync(basket); [Route("draft/{basketId}")]
[HttpGet]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{
if (string.IsNullOrEmpty(basketId))
{
return BadRequest("Need a valid basketid");
}
// Get the basket data and build a order draft based on it
var basket = await _basketService.GetById(basketId);
if (basket == null)
{
return BadRequest($"No basket found for id {basketId}");
}
return await _orderingService.GetOrderDraftAsync(basket);
}
} }
} }

View File

@ -1,8 +1,8 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app WORKDIR /app
EXPOSE 80 EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src WORKDIR /src
# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles # It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles
@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe
COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj"
COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj"
COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj"
COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj"
COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj"
COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj"
@ -32,8 +33,6 @@ 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"
@ -43,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj"
COPY "docker-compose.dcproj" "docker-compose.dcproj" COPY "docker-compose.dcproj" "docker-compose.dcproj"
COPY "Directory.Packages.props" "Directory.Packages.props"
COPY "NuGet.config" "NuGet.config" COPY "NuGet.config" "NuGet.config"
RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0 FROM mcr.microsoft.com/dotnet/sdk:5.0
ARG BUILD_CONFIGURATION=Debug ARG BUILD_CONFIGURATION=Debug
ENV ASPNETCORE_ENVIRONMENT=Development ENV ASPNETCORE_ENVIRONMENT=Development
ENV DOTNET_USE_POLLING_FILE_WATCHER=true ENV DOTNET_USE_POLLING_FILE_WATCHER=true
@ -6,6 +6,7 @@ EXPOSE 80
WORKDIR /src WORKDIR /src
COPY ["src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj", "src/ApiGateways/Mobile.Bff.Shopping/aggregator/"] COPY ["src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj", "src/ApiGateways/Mobile.Bff.Shopping/aggregator/"]
COPY ["src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "src/BuildingBlocks/Devspaces.Support/"]
COPY ["src/NuGet.config", "src/NuGet.config"] COPY ["src/NuGet.config", "src/NuGet.config"]
RUN dotnet restore src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503 RUN dotnet restore src/ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503

View File

@ -1,62 +0,0 @@
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

@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;
using System.Linq;
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 OpenApiSecurityRequirement
{
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" }
}
};
}
}
}
}

View File

@ -1,13 +0,0 @@
global using System.Text.Json;
global using CatalogApi;
global using Grpc.Core;
global using Grpc.Core.Interceptors;
global using GrpcBasket;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
global using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
global using Microsoft.Extensions.Options;
global using Services.Common;

View File

@ -1,35 +1,41 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
public class GrpcExceptionInterceptor : Interceptor namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure
{ {
private readonly ILogger<GrpcExceptionInterceptor> _logger; public class GrpcExceptionInterceptor : Interceptor
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
{ {
_logger = logger; private readonly ILogger<GrpcExceptionInterceptor> _logger;
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
{
try
{ {
var response = await t; _logger = logger;
return response;
} }
catch (RpcException e)
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{ {
_logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); var call = continuation(request, context);
return default;
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
{
try
{
var response = await t;
return response;
}
catch (RpcException e)
{
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
return default;
}
} }
} }
} }

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
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

@ -1,23 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<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>
<ImplicitUsings>enable</ImplicitUsings> <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled>
<LangVersion>preview</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" /> <Folder Include="wwwroot\" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> <PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="5.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.14.0" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Core" Version="2.34.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\Devspaces.Support\Devspaces.Support.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,15 +1,16 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class AddBasketItemRequest
{ {
public int CatalogItemId { get; set; } public class AddBasketItemRequest
public string BasketId { get; set; }
public int Quantity { get; set; }
public AddBasketItemRequest()
{ {
Quantity = 1; public int CatalogItemId { get; set; }
public string BasketId { get; set; }
public int Quantity { get; set; }
public AddBasketItemRequest()
{
Quantity = 1;
}
} }
} }

View File

@ -1,17 +1,22 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class BasketData namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
{ {
public string BuyerId { get; set; }
public List<BasketDataItem> Items { get; set; } = new(); public class BasketData
public BasketData()
{ {
public string BuyerId { get; set; }
public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>();
public BasketData()
{
}
public BasketData(string buyerId)
{
BuyerId = buyerId;
}
} }
public BasketData(string buyerId)
{
BuyerId = buyerId;
}
} }

View File

@ -1,18 +1,21 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class BasketDataItem
{ {
public string Id { get; set; }
public int ProductId { get; set; } public class BasketDataItem
{
public string Id { get; set; }
public string ProductName { get; set; } public int ProductId { get; set; }
public decimal UnitPrice { get; set; } public string ProductName { get; set; }
public decimal OldUnitPrice { get; set; } public decimal UnitPrice { get; set; }
public int Quantity { get; set; } public decimal OldUnitPrice { get; set; }
public int Quantity { get; set; }
public string PictureUrl { get; set; }
}
public string PictureUrl { get; set; }
} }

View File

@ -1,12 +1,13 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class CatalogItem
{ {
public int Id { get; set; } public class CatalogItem
{
public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public decimal Price { get; set; } public decimal Price { get; set; }
public string PictureUri { get; set; } public string PictureUri { get; set; }
}
} }

View File

@ -1,42 +1,48 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; using System;
using System.Collections.Generic;
public class OrderData namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
{ {
public string OrderNumber { get; set; }
public DateTime Date { get; set; } public class OrderData
{
public string OrderNumber { get; set; }
public string Status { get; set; } public DateTime Date { get; set; }
public decimal Total { get; set; } public string Status { get; set; }
public string Description { get; set; } public decimal Total { get; set; }
public string City { get; set; } public string Description { get; set; }
public string Street { get; set; } public string City { get; set; }
public string State { get; set; } public string Street { get; set; }
public string Country { get; set; } public string State { get; set; }
public string ZipCode { get; set; } public string Country { get; set; }
public string CardNumber { get; set; } public string ZipCode { get; set; }
public string CardHolderName { get; set; } public string CardNumber { get; set; }
public bool IsDraft { get; set; } public string CardHolderName { get; set; }
public DateTime CardExpiration { get; set; } public bool IsDraft { get; set; }
public string CardExpirationShort { get; set; } public DateTime CardExpiration { get; set; }
public string CardSecurityNumber { get; set; } public string CardExpirationShort { get; set; }
public int CardTypeId { get; set; } public string CardSecurityNumber { get; set; }
public string Buyer { get; set; } public int CardTypeId { get; set; }
public string Buyer { get; set; }
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>();
}
public List<OrderItemData> OrderItems { get; } = new();
} }

View File

@ -1,16 +1,19 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class OrderItemData
{ {
public int ProductId { get; set; }
public string ProductName { get; set; } public class OrderItemData
{
public int ProductId { get; set; }
public decimal UnitPrice { get; set; } public string ProductName { get; set; }
public decimal Discount { get; set; } public decimal UnitPrice { get; set; }
public int Units { get; set; } public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
public string PictureUrl { get; set; }
} }

View File

@ -1,13 +1,16 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class UpdateBasketItemData
{ {
public string BasketItemId { get; set; }
public int NewQty { get; set; } public class UpdateBasketItemData
public UpdateBasketItemData()
{ {
NewQty = 0; public string BasketItemId { get; set; }
public int NewQty { get; set; }
public UpdateBasketItemData()
{
NewQty = 0;
}
} }
} }

View File

@ -1,13 +1,19 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class UpdateBasketItemsRequest namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
{ {
public string BasketId { get; set; }
public ICollection<UpdateBasketItemData> Updates { get; set; } public class UpdateBasketItemsRequest
public UpdateBasketItemsRequest()
{ {
Updates = new List<UpdateBasketItemData>();
public string BasketId { get; set; }
public ICollection<UpdateBasketItemData> Updates { get; set; }
public UpdateBasketItemsRequest()
{
Updates = new List<UpdateBasketItemData>();
}
} }
} }

View File

@ -1,8 +1,13 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class UpdateBasketRequest namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
{ {
public string BuyerId { get; set; }
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } public class UpdateBasketRequest
} {
public string BuyerId { get; set; }
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; }
}
}

View File

@ -1,10 +1,13 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models
public class UpdateBasketRequestItemData
{ {
public string Id { get; set; } // Basket id
public int ProductId { get; set; } // Catalog item id public class UpdateBasketRequestItemData
{
public string Id { get; set; } // Basket id
public int ProductId { get; set; } // Catalog item id
public int Quantity { get; set; } // Quantity
}
public int Quantity { get; set; } // Quantity
} }

View File

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

View File

@ -24,6 +24,13 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"applicationUrl": "http://localhost:61632/" "applicationUrl": "http://localhost:61632/"
},
"Azure Dev Spaces": {
"commandName": "AzureDevSpaces",
"launchBrowser": true,
"resourceGroup": "eshoptestedu",
"aksName": "eshoptestedu",
"subscriptionId": "e3035ac1-c06c-4daf-8939-57b3c5f1f759"
} }
} }
} }

View File

@ -1,83 +1,90 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using GrpcBasket;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
public class BasketService : IBasketService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
private readonly Basket.BasketClient _basketClient; public class BasketService : IBasketService
private readonly ILogger<BasketService> _logger;
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger)
{ {
_basketClient = basketClient; private readonly Basket.BasketClient _basketClient;
_logger = logger; private readonly ILogger<BasketService> _logger;
}
public async Task<BasketData> GetByIdAsync(string id) public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger)
{
_logger.LogDebug("grpc client created, request = {@id}", id);
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
_logger.LogDebug("grpc response {@response}", response);
return MapToBasketData(response);
}
public async Task UpdateAsync(BasketData currentBasket)
{
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket);
var request = MapToCustomerBasketRequest(currentBasket);
_logger.LogDebug("Grpc update basket request {@request}", request);
await _basketClient.UpdateBasketAsync(request);
}
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
{
if (customerBasketRequest == null)
{ {
return null; _basketClient = basketClient;
_logger = logger;
} }
var map = new BasketData public async Task<BasketData> GetById(string id)
{ {
BuyerId = customerBasketRequest.Buyerid _logger.LogDebug("grpc client created, request = {@id}", id);
}; var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
_logger.LogDebug("grpc response {@response}", response);
customerBasketRequest.Items.ToList().ForEach(item => map.Items.Add(new BasketDataItem return MapToBasketData(response);
{
Id = item.Id,
OldUnitPrice = (decimal)item.Oldunitprice,
PictureUrl = item.Pictureurl,
ProductId = item.Productid,
ProductName = item.Productname,
Quantity = item.Quantity,
UnitPrice = (decimal)item.Unitprice
}));
return map;
}
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
{
if (basketData == null)
{
return null;
} }
var map = new CustomerBasketRequest public async Task UpdateAsync(BasketData currentBasket)
{ {
Buyerid = basketData.BuyerId _logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket);
}; var request = MapToCustomerBasketRequest(currentBasket);
_logger.LogDebug("Grpc update basket request {@request}", request);
basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse await _basketClient.UpdateBasketAsync(request);
}
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
{ {
Id = item.Id, if (customerBasketRequest == null)
Oldunitprice = (double)item.OldUnitPrice, {
Pictureurl = item.PictureUrl, return null;
Productid = item.ProductId, }
Productname = item.ProductName,
Quantity = item.Quantity,
Unitprice = (double)item.UnitPrice
}));
return map; var map = new BasketData
{
BuyerId = customerBasketRequest.Buyerid
};
customerBasketRequest.Items.ToList().ForEach(item => map.Items.Add(new BasketDataItem
{
Id = item.Id,
OldUnitPrice = (decimal)item.Oldunitprice,
PictureUrl = item.Pictureurl,
ProductId = item.Productid,
ProductName = item.Productname,
Quantity = item.Quantity,
UnitPrice = (decimal)item.Unitprice
}));
return map;
}
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
{
if (basketData == null)
{
return null;
}
var map = new CustomerBasketRequest
{
Buyerid = basketData.BuyerId
};
basketData.Items.ToList().ForEach(item => map.Items.Add(new BasketItemResponse
{
Id = item.Id,
Oldunitprice = (double)item.OldUnitPrice,
Pictureurl = item.PictureUrl,
Productid = item.ProductId,
Productname = item.ProductName,
Quantity = item.Quantity,
Unitprice = (double)item.UnitPrice
}));
return map;
}
} }
} }

View File

@ -1,36 +1,43 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using CatalogApi;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CatalogService : ICatalogService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
private readonly Catalog.CatalogClient _client; public class CatalogService : ICatalogService
public CatalogService(Catalog.CatalogClient client)
{ {
_client = client; private readonly Catalog.CatalogClient _client;
}
public async Task<CatalogItem> GetCatalogItemAsync(int id) public CatalogService(Catalog.CatalogClient client)
{
var request = new CatalogItemRequest { Id = id };
var response = await _client.GetItemByIdAsync(request);
return MapToCatalogItemResponse(response);
}
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
{
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 };
var response = await _client.GetItemsByIdsAsync(request);
return response.Data.Select(MapToCatalogItemResponse);
}
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse)
{
return new CatalogItem
{ {
Id = catalogItemResponse.Id, _client = client;
Name = catalogItemResponse.Name, }
PictureUri = catalogItemResponse.PictureUri,
Price = (decimal)catalogItemResponse.Price public async Task<CatalogItem> GetCatalogItemAsync(int id)
}; {
var request = new CatalogItemRequest { Id = id };
var response = await _client.GetItemByIdAsync(request);
return MapToCatalogItemResponse(response);
}
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
{
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 };
var response = await _client.GetItemsByIdsAsync(request);
return response.Data.Select(MapToCatalogItemResponse);
}
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse)
{
return new CatalogItem
{
Id = catalogItemResponse.Id,
Name = catalogItemResponse.Name,
PictureUri = catalogItemResponse.PictureUri,
Price = (decimal)catalogItemResponse.Price
};
}
} }
} }

View File

@ -1,8 +1,13 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IBasketService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
Task<BasketData> GetByIdAsync(string id); public interface IBasketService
{
Task<BasketData> GetById(string id);
Task UpdateAsync(BasketData currentBasket); Task UpdateAsync(BasketData currentBasket);
}
} }

View File

@ -1,8 +1,13 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
public interface ICatalogService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
Task<CatalogItem> GetCatalogItemAsync(int id); public interface ICatalogService
{
Task<CatalogItem> GetCatalogItemAsync(int id);
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids);
}
} }

View File

@ -1,6 +1,10 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IOrderApiClient namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); public interface IOrderApiClient
{
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket);
}
} }

View File

@ -1,6 +1,10 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IOrderingService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
Task<OrderData> GetOrderDraftAsync(BasketData basketData); public interface IOrderingService
} {
Task<OrderData> GetOrderDraftAsync(BasketData basketData);
}
}

View File

@ -1,28 +1,40 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
public class OrderApiClient : IOrderApiClient namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
private readonly HttpClient _apiClient; public class OrderApiClient : IOrderApiClient
private readonly ILogger<OrderApiClient> _logger;
private readonly UrlsConfig _urls;
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config)
{ {
_apiClient = httpClient; private readonly HttpClient _apiClient;
_logger = logger; private readonly ILogger<OrderApiClient> _logger;
_urls = config.Value; private readonly UrlsConfig _urls;
}
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config)
{ {
var uri = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft(); _apiClient = httpClient;
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); _logger = logger;
var response = await _apiClient.PostAsync(uri, content); _urls = config.Value;
}
response.EnsureSuccessStatusCode(); public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket)
{
var uri = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft();
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json");
var response = await _apiClient.PostAsync(uri, content);
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions); var ordersDraftResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
} }
} }

View File

@ -1,72 +1,79 @@
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services; using GrpcOrdering;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
public class OrderingService : IOrderingService namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services
{ {
private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; public class OrderingService : IOrderingService
private readonly ILogger<OrderingService> _logger;
public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{ {
_orderingGrpcClient = orderingGrpcClient; private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient;
_logger = logger; private readonly ILogger<OrderingService> _logger;
}
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
var command = MapToOrderDraftCommand(basketData);
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command);
_logger.LogDebug(" grpc response: {@response}", response);
return MapToResponse(response, basketData);
}
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData)
{
if (orderDraft == null)
{ {
return null; _orderingGrpcClient = orderingGrpcClient;
_logger = logger;
} }
var data = new OrderData public async Task<OrderData> GetOrderDraftAsync(BasketData basketData)
{ {
Buyer = basketData.BuyerId, _logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
Total = (decimal)orderDraft.Total,
};
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData var command = MapToOrderDraftCommand(basketData);
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command);
_logger.LogDebug(" grpc response: {@response}", response);
return MapToResponse(response, basketData);
}
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData)
{ {
Discount = (decimal)o.Discount, if (orderDraft == null)
PictureUrl = o.PictureUrl, {
ProductId = o.ProductId, return null;
ProductName = o.ProductName, }
UnitPrice = (decimal)o.UnitPrice,
Units = o.Units, var data = new OrderData
})); {
Buyer = basketData.BuyerId,
Total = (decimal)orderDraft.Total,
};
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData
{
Discount = (decimal)o.Discount,
PictureUrl = o.PictureUrl,
ProductId = o.ProductId,
ProductName = o.ProductName,
UnitPrice = (decimal)o.UnitPrice,
Units = o.Units,
}));
return data;
}
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{
var command = new CreateOrderDraftCommand
{
BuyerId = basketData.BuyerId,
};
basketData.Items.ForEach(i => command.Items.Add(new BasketItem
{
Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice,
PictureUrl = i.PictureUrl,
ProductId = i.ProductId,
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = (double)i.UnitPrice,
}));
return command;
}
return data;
} }
private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{
var command = new GrpcOrdering.CreateOrderDraftCommand
{
BuyerId = basketData.BuyerId,
};
basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem
{
Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice,
PictureUrl = i.PictureUrl,
ProductId = i.ProductId,
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = (double)i.UnitPrice,
}));
return command;
}
} }

View File

@ -0,0 +1,222 @@
using CatalogApi;
using Devspaces.Support;
using GrpcBasket;
using GrpcOrdering;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Config;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure;
using Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
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)
.AddDevspaces()
.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";
});
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>()
.AddDevspacesSupport();
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,138 +1,15 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "IncludeScopes": false,
"Default": "Information", "Debug": {
"Microsoft.AspNetCore": "Warning", "LogLevel": {
"System.Net.Http": "Warning" "Default": "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}"
}
} }
}, },
"Clusters": { "Console": {
"basket": { "LogLevel": {
"Destinations": { "Default": "Warning"
"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,19 +8,16 @@
"grpcCatalog": "http://localhost:81", "grpcCatalog": "http://localhost:81",
"grpcOrdering": "http://localhost:5581" "grpcOrdering": "http://localhost:5581"
}, },
"Identity": { "IdentityUrlExternal": "http://localhost:5105",
"ExternalUrl": "http://localhost:5105", "IdentityUrl": "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

@ -0,0 +1,55 @@
kind: helm-release
apiVersion: 1.1
build:
context: ..\..\..\..
dockerfile: Dockerfile
install:
chart: ../../../../k8s/helm/mobileshoppingagg
set:
image:
tag: $(tag)
pullPolicy: Never
ingress:
annotations:
kubernetes.io/ingress.class: traefik-azds
hosts:
# This expands to [space.s.]apigwms.<guid>.<region>.aksapp.io
- $(spacePrefix)eshop$(hostSuffix)
inf:
k8s:
dns: $(spacePrefix)eshop$(hostSuffix)
values:
- values.dev.yaml?
- secrets.dev.yaml?
- app.yaml
- inf.yaml
configurations:
develop:
build:
useGitIgnore: true
dockerfile: Dockerfile.develop
container:
syncTarget: /src
sync:
- '**/Pages/**'
- '**/Views/**'
- '**/wwwroot/**'
- '!**/*.{sln,csproj}'
command:
- dotnet
- run
- --no-restore
- --no-build
- --no-launch-profile
- -c
- ${Configuration:-Debug}
iterate:
processesToKill:
- dotnet
- vsdbg
buildCommands:
- - dotnet
- build
- --no-restore
- -c
- ${Configuration:-Debug}

View File

@ -1,41 +1,45 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; using System.Collections.Generic;
public class UrlsConfig namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config
{ {
public class CatalogOperations public class UrlsConfig
{ {
// grpc call under REST must go trough port 80
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}";
public static string GetItemById(string ids) => $"/api/v1/catalog/items/ids/{string.Join(',', ids)}"; public class CatalogOperations
{
// grpc call under REST must go trough port 80
public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}";
// REST call standard must go through port 5000 public static string GetItemById(string ids) => $"/api/v1/catalog/items/ids/{string.Join(',', ids)}";
public static string GetItemsById(IEnumerable<int> ids) => $":5000/api/v1/catalog/items?ids={string.Join(',', ids)}";
// REST call standard must go through port 5000
public static string GetItemsById(IEnumerable<int> ids) => $":5000/api/v1/catalog/items?ids={string.Join(',', ids)}";
}
public class BasketOperations
{
public static string GetItemById(string id) => $"/api/v1/basket/{id}";
public static string UpdateBasket() => "/api/v1/basket";
}
public class OrdersOperations
{
public static string GetOrderDraft() => "/api/v1/orders/draft";
}
public string Basket { get; set; }
public string Catalog { get; set; }
public string Orders { get; set; }
public string GrpcBasket { get; set; }
public string GrpcCatalog { get; set; }
public string GrpcOrdering { get; set; }
} }
public class BasketOperations
{
public static string GetItemById(string id) => $"/api/v1/basket/{id}";
public static string UpdateBasket() => "/api/v1/basket";
}
public class OrdersOperations
{
public static string GetOrderDraft() => "/api/v1/orders/draft";
}
public string Basket { get; set; }
public string Catalog { get; set; }
public string Orders { get; set; }
public string GrpcBasket { get; set; }
public string GrpcCatalog { get; set; }
public string GrpcOrdering { get; set; }
} }

View File

@ -1,152 +1,164 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
[Route("api/v1/[controller]")] namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers
[Authorize]
[ApiController]
public class BasketController : ControllerBase
{ {
private readonly ICatalogService _catalog; [Route("api/v1/[controller]")]
private readonly IBasketService _basket; [Authorize]
[ApiController]
public BasketController(ICatalogService catalogService, IBasketService basketService) public class BasketController : ControllerBase
{ {
_catalog = catalogService; private readonly ICatalogService _catalog;
_basket = basketService; private readonly IBasketService _basket;
}
[HttpPost] public BasketController(ICatalogService catalogService, IBasketService basketService)
[HttpPut]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
{
if (data.Items == null || !data.Items.Any())
{ {
return BadRequest("Need to pass at least one basket line"); _catalog = catalogService;
_basket = basketService;
} }
// Retrieve the current basket [HttpPost]
var basket = await _basket.GetByIdAsync(data.BuyerId) ?? new BasketData(data.BuyerId); [HttpPut]
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId)); [ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
// group by product id to avoid duplicates public async Task<ActionResult<BasketData>> UpdateAllBasketAsync([FromBody] UpdateBasketRequest data)
var itemsCalculated = data
.Items
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i })
.Select(groupedItem =>
{
var item = groupedItem.items.First();
item.Quantity = groupedItem.items.Sum(i => i.Quantity);
return item;
});
foreach (var bitem in itemsCalculated)
{ {
var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); if (data.Items == null || !data.Items.Any())
if (catalogItem == null)
{ {
return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); return BadRequest("Need to pass at least one basket line");
} }
var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId); // Retrieve the current basket
if (itemInBasket == null) var basket = await _basket.GetById(data.BuyerId) ?? new BasketData(data.BuyerId);
var catalogItems = await _catalog.GetCatalogItemsAsync(data.Items.Select(x => x.ProductId));
// group by product id to avoid duplicates
var itemsCalculated = data
.Items
.GroupBy(x => x.ProductId, x => x, (k, i) => new { productId = k, items = i })
.Select(groupedItem =>
{
var item = groupedItem.items.First();
item.Quantity = groupedItem.items.Sum(i => i.Quantity);
return item;
});
foreach (var bitem in itemsCalculated)
{ {
basket.Items.Add(new BasketDataItem() var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId);
if (catalogItem == null)
{ {
Id = bitem.Id, return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})");
ProductId = catalogItem.Id, }
ProductName = catalogItem.Name,
PictureUrl = catalogItem.PictureUri, var itemInBasket = basket.Items.FirstOrDefault(x => x.ProductId == bitem.ProductId);
UnitPrice = catalogItem.Price, if (itemInBasket == null)
Quantity = bitem.Quantity {
}); basket.Items.Add(new BasketDataItem()
{
Id = bitem.Id,
ProductId = catalogItem.Id,
ProductName = catalogItem.Name,
PictureUrl = catalogItem.PictureUri,
UnitPrice = catalogItem.Price,
Quantity = bitem.Quantity
});
}
else
{
itemInBasket.Quantity = bitem.Quantity;
}
}
await _basket.UpdateAsync(basket);
return basket;
}
[HttpPut]
[Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(BasketData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{
if (!data.Updates.Any())
{
return BadRequest("No updates sent");
}
// Retrieve the current basket
var currentBasket = await _basket.GetById(data.BasketId);
if (currentBasket == null)
{
return BadRequest($"Basket with id {data.BasketId} not found.");
}
// Update with new quantities
foreach (var update in data.Updates)
{
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId);
if (basketItem == null)
{
return BadRequest($"Basket item with id {update.BasketItemId} not found");
}
basketItem.Quantity = update.NewQty;
}
// Save the updated basket
await _basket.UpdateAsync(currentBasket);
return currentBasket;
}
[HttpPost]
[Route("items")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
{
if (data == null || data.Quantity == 0)
{
return BadRequest("Invalid payload");
}
// Step 1: Get the item from catalog
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId);
//item.PictureUri =
// Step 2: Get current basket status
var currentBasket = (await _basket.GetById(data.BasketId)) ?? new BasketData(data.BasketId);
// Step 3: Search if exist product into basket
var product = currentBasket.Items.SingleOrDefault(i => i.ProductId == item.Id);
if (product != null)
{
// Step 4: Update quantity for product
product.Quantity += data.Quantity;
} }
else else
{ {
itemInBasket.Quantity = bitem.Quantity; // Step 4: Merge current status with new product
currentBasket.Items.Add(new BasketDataItem()
{
UnitPrice = item.Price,
PictureUrl = item.PictureUri,
ProductId = item.Id,
ProductName = item.Name,
Quantity = data.Quantity,
Id = Guid.NewGuid().ToString()
});
} }
// Step 5: Update basket
await _basket.UpdateAsync(currentBasket);
return Ok();
} }
await _basket.UpdateAsync(basket);
return basket;
}
[HttpPut]
[Route("items")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BasketData>> UpdateQuantitiesAsync([FromBody] UpdateBasketItemsRequest data)
{
if (!data.Updates.Any())
{
return BadRequest("No updates sent");
}
// Retrieve the current basket
var currentBasket = await _basket.GetByIdAsync(data.BasketId);
if (currentBasket == null)
{
return BadRequest($"Basket with id {data.BasketId} not found.");
}
// Update with new quantities
foreach (var update in data.Updates)
{
var basketItem = currentBasket.Items.SingleOrDefault(bitem => bitem.Id == update.BasketItemId);
if (basketItem == null)
{
return BadRequest($"Basket item with id {update.BasketItemId} not found");
}
basketItem.Quantity = update.NewQty;
}
// Save the updated basket
await _basket.UpdateAsync(currentBasket);
return currentBasket;
}
[HttpPost]
[Route("items")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> AddBasketItemAsync([FromBody] AddBasketItemRequest data)
{
if (data == null || data.Quantity == 0)
{
return BadRequest("Invalid payload");
}
// Step 1: Get the item from catalog
var item = await _catalog.GetCatalogItemAsync(data.CatalogItemId);
//item.PictureUri =
// Step 2: Get current basket status
var currentBasket = (await _basket.GetByIdAsync(data.BasketId)) ?? new BasketData(data.BasketId);
// Step 3: Search if exist product into basket
var product = currentBasket.Items.SingleOrDefault(i => i.ProductId == item.Id);
if (product != null)
{
// Step 4: Update quantity for product
product.Quantity += data.Quantity;
}
else
{
// Step 4: Merge current status with new product
currentBasket.Items.Add(new BasketDataItem()
{
UnitPrice = item.Price,
PictureUrl = item.PictureUri,
ProductId = item.Id,
ProductName = item.Name,
Quantity = data.Quantity,
Id = Guid.NewGuid().ToString()
});
}
// Step 5: Update basket
await _basket.UpdateAsync(currentBasket);
return Ok();
} }
} }

View File

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

View File

@ -1,36 +1,44 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
using System.Net;
using System.Threading.Tasks;
[Route("api/v1/[controller]")] namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers
[Authorize]
[ApiController]
public class OrderController : ControllerBase
{ {
private readonly IBasketService _basketService; [Route("api/v1/[controller]")]
private readonly IOrderingService _orderingService; [Authorize]
[ApiController]
public OrderController(IBasketService basketService, IOrderingService orderingService) public class OrderController : ControllerBase
{ {
_basketService = basketService; private readonly IBasketService _basketService;
_orderingService = orderingService; private readonly IOrderingService _orderingService;
} public OrderController(IBasketService basketService, IOrderingService orderingService)
[Route("draft/{basketId}")]
[HttpGet]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{
if (string.IsNullOrWhiteSpace(basketId))
{ {
return BadRequest("Need a valid basketid"); _basketService = basketService;
} _orderingService = orderingService;
// Get the basket data and build a order draft based on it
var basket = await _basketService.GetByIdAsync(basketId);
if (basket == null)
{
return BadRequest($"No basket found for id {basketId}");
} }
return await _orderingService.GetOrderDraftAsync(basket); [Route("draft/{basketId}")]
[HttpGet]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType(typeof(OrderData), (int)HttpStatusCode.OK)]
public async Task<ActionResult<OrderData>> GetOrderDraftAsync(string basketId)
{
if (string.IsNullOrEmpty(basketId))
{
return BadRequest("Need a valid basketid");
}
// Get the basket data and build a order draft based on it
var basket = await _basketService.GetById(basketId);
if (basket == null)
{
return BadRequest($"No basket found for id {basketId}");
}
return await _orderingService.GetOrderDraftAsync(basket);
}
} }
} }

View File

@ -1,8 +1,8 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app WORKDIR /app
EXPOSE 80 EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src WORKDIR /src
# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles # It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles
@ -11,6 +11,7 @@ COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWe
COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj"
COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj"
COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj"
COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj"
COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj"
COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj"
@ -32,8 +33,6 @@ 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"
@ -43,7 +42,6 @@ COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj"
COPY "docker-compose.dcproj" "docker-compose.dcproj" COPY "docker-compose.dcproj" "docker-compose.dcproj"
COPY "Directory.Packages.props" "Directory.Packages.props"
COPY "NuGet.config" "NuGet.config" COPY "NuGet.config" "NuGet.config"
RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln"

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0 FROM mcr.microsoft.com/dotnet/sdk:5.0
ARG BUILD_CONFIGURATION=Debug ARG BUILD_CONFIGURATION=Debug
ENV ASPNETCORE_ENVIRONMENT=Development ENV ASPNETCORE_ENVIRONMENT=Development
ENV DOTNET_USE_POLLING_FILE_WATCHER=true ENV DOTNET_USE_POLLING_FILE_WATCHER=true
@ -6,6 +6,7 @@ EXPOSE 80
WORKDIR /src WORKDIR /src
COPY ["src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj", "src/ApiGateways/Web.Bff.Shopping/aggregator/"] COPY ["src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj", "src/ApiGateways/Web.Bff.Shopping/aggregator/"]
COPY ["src/BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj", "src/BuildingBlocks/Devspaces.Support/"]
COPY ["src/NuGet.config", "src/NuGet.config"] COPY ["src/NuGet.config", "src/NuGet.config"]
RUN dotnet restore src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503 RUN dotnet restore src/ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj -nowarn:msb3202,nu1503

View File

@ -1,63 +0,0 @@
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

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;
using System.Linq;
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 OpenApiSecurityRequirement
{
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" }
}
};
}
}
}
}

View File

@ -1,13 +0,0 @@
global using System.Text.Json;
global using CatalogApi;
global using Grpc.Core;
global using Grpc.Core.Interceptors;
global using GrpcBasket;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
global using Microsoft.Extensions.Options;
global using Services.Common;

View File

@ -1,35 +1,41 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
public class GrpcExceptionInterceptor : Interceptor namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure
{ {
private readonly ILogger<GrpcExceptionInterceptor> _logger; public class GrpcExceptionInterceptor : Interceptor
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
{ {
_logger = logger; private readonly ILogger<GrpcExceptionInterceptor> _logger;
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> task)
{
try
{ {
var response = await task; _logger = logger;
return response;
} }
catch (RpcException e)
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{ {
_logger.LogError(e, "Error calling via gRPC: {Status}", e.Status); var call = continuation(request, context);
return default;
return new AsyncUnaryCall<TResponse>(HandleResponse(call.ResponseAsync), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> t)
{
try
{
var response = await t;
return response;
}
catch (RpcException e)
{
_logger.LogError("Error calling via grpc: {Status} - {Message}", e.Status, e.Message);
return default;
}
} }
} }
} }

View File

@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
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.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

@ -1,16 +1,18 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class AddBasketItemRequest
{ {
public int CatalogItemId { get; set; }
public string BasketId { get; set; } public class AddBasketItemRequest
public int Quantity { get; set; }
public AddBasketItemRequest()
{ {
Quantity = 1; public int CatalogItemId { get; set; }
}
}
public string BasketId { get; set; }
public int Quantity { get; set; }
public AddBasketItemRequest()
{
Quantity = 1;
}
}
}

View File

@ -1,18 +1,22 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class BasketData namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
{ {
public string BuyerId { get; set; }
public List<BasketDataItem> Items { get; set; } = new(); public class BasketData
public BasketData()
{ {
public string BuyerId { get; set; }
public List<BasketDataItem> Items { get; set; } = new List<BasketDataItem>();
public BasketData()
{
}
public BasketData(string buyerId)
{
BuyerId = buyerId;
}
} }
public BasketData(string buyerId)
{
BuyerId = buyerId;
}
} }

View File

@ -1,18 +1,21 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class BasketDataItem
{ {
public string Id { get; set; }
public int ProductId { get; set; } public class BasketDataItem
{
public string Id { get; set; }
public string ProductName { get; set; } public int ProductId { get; set; }
public decimal UnitPrice { get; set; } public string ProductName { get; set; }
public decimal OldUnitPrice { get; set; } public decimal UnitPrice { get; set; }
public int Quantity { get; set; } public decimal OldUnitPrice { get; set; }
public int Quantity { get; set; }
public string PictureUrl { get; set; }
}
public string PictureUrl { get; set; }
} }

View File

@ -1,14 +1,15 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class CatalogItem
{ {
public int Id { get; set; }
public string Name { get; set; } public class CatalogItem
{
public int Id { get; set; }
public decimal Price { get; set; } public string Name { get; set; }
public decimal Price { get; set; }
public string PictureUri { get; set; }
}
public string PictureUri { get; set; }
} }

View File

@ -1,43 +1,48 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; using System;
using System.Collections.Generic;
public class OrderData namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
{ {
public string OrderNumber { get; set; }
public DateTime Date { get; set; } public class OrderData
{
public string OrderNumber { get; set; }
public string Status { get; set; } public DateTime Date { get; set; }
public decimal Total { get; set; } public string Status { get; set; }
public string Description { get; set; } public decimal Total { get; set; }
public string City { get; set; } public string Description { get; set; }
public string Street { get; set; } public string City { get; set; }
public string State { get; set; } public string Street { get; set; }
public string Country { get; set; } public string State { get; set; }
public string ZipCode { get; set; } public string Country { get; set; }
public string CardNumber { get; set; } public string ZipCode { get; set; }
public string CardHolderName { get; set; } public string CardNumber { get; set; }
public bool IsDraft { get; set; } public string CardHolderName { get; set; }
public DateTime CardExpiration { get; set; } public bool IsDraft { get; set; }
public string CardExpirationShort { get; set; } public DateTime CardExpiration { get; set; }
public string CardSecurityNumber { get; set; } public string CardExpirationShort { get; set; }
public int CardTypeId { get; set; } public string CardSecurityNumber { get; set; }
public string Buyer { get; set; } public int CardTypeId { get; set; }
public string Buyer { get; set; }
public List<OrderItemData> OrderItems { get; } = new List<OrderItemData>();
}
public List<OrderItemData> OrderItems { get; } = new();
} }

View File

@ -1,16 +1,19 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class OrderItemData
{ {
public int ProductId { get; set; }
public string ProductName { get; set; } public class OrderItemData
{
public int ProductId { get; set; }
public decimal UnitPrice { get; set; } public string ProductName { get; set; }
public decimal Discount { get; set; } public decimal UnitPrice { get; set; }
public int Units { get; set; } public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
public string PictureUrl { get; set; }
} }

View File

@ -1,9 +1,16 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class UpdateBasketItemData
{ {
public string BasketItemId { get; set; }
public int NewQty { get; set; } public class UpdateBasketItemData
{
public string BasketItemId { get; set; }
public int NewQty { get; set; }
public UpdateBasketItemData()
{
NewQty = 0;
}
}
} }

View File

@ -1,13 +1,18 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class UpdateBasketItemsRequest namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
{ {
public string BasketId { get; set; }
public ICollection<UpdateBasketItemData> Updates { get; set; } public class UpdateBasketItemsRequest
public UpdateBasketItemsRequest()
{ {
Updates = new List<UpdateBasketItemData>(); public string BasketId { get; set; }
public ICollection<UpdateBasketItemData> Updates { get; set; }
public UpdateBasketItemsRequest()
{
Updates = new List<UpdateBasketItemData>();
}
} }
} }

View File

@ -1,8 +1,13 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; using System.Collections.Generic;
public class UpdateBasketRequest namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
{ {
public string BuyerId { get; set; }
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; } public class UpdateBasketRequest
{
public string BuyerId { get; set; }
public IEnumerable<UpdateBasketRequestItemData> Items { get; set; }
}
} }

View File

@ -1,10 +1,11 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models
public class UpdateBasketRequestItemData
{ {
public string Id { get; set; } // Basket id public class UpdateBasketRequestItemData
{
public string Id { get; set; } // Basket id
public int ProductId { get; set; } // Catalog item id public int ProductId { get; set; } // Catalog item id
public int Quantity { get; set; } // Quantity public int Quantity { get; set; } // Quantity
}
} }

View File

@ -1,38 +1,29 @@
var builder = WebApplication.CreateBuilder(args); using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator;
using Serilog;
builder.AddServiceDefaults(); BuildWebHost(args).Run();
builder.Services.AddReverseProxy(builder.Configuration); IWebHost BuildWebHost(string[] args) =>
builder.Services.AddControllers(); WebHost
.CreateDefaultBuilder(args)
builder.Services.AddHealthChecks(builder.Configuration); .ConfigureAppConfiguration(cb =>
builder.Services.AddCors(options => {
{ var sources = cb.Sources;
// TODO: Read allowed origins from configuration sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource()
options.AddPolicy("CorsPolicy", {
builder => builder Optional = true,
.SetIsOriginAllowed((host) => true) Path = "appsettings.localhost.json",
.AllowAnyMethod() ReloadOnChange = false
.AllowAnyHeader() });
.AllowCredentials()); })
}); .UseStartup<Startup>()
.UseSerilog((builderContext, config) =>
builder.Services.AddApplicationServices(); {
builder.Services.AddGrpcServices(); config
.MinimumLevel.Information()
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); .Enrich.FromLogContext()
.WriteTo.Console();
var app = builder.Build(); })
.Build();
app.UseServiceDefaults();
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapReverseProxy();
await app.RunAsync();

View File

@ -1,12 +1,29 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:57425/",
"sslPort": 0
}
},
"profiles": { "profiles": {
"Web.Shopping.HttpAggregator": { "IIS Express": {
"commandName": "Project", "commandName": "IISExpress",
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5229/", "launchUrl": "api/values",
"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

@ -1,95 +1,103 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using GrpcBasket;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
public class BasketService : IBasketService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
private readonly Basket.BasketClient _basketClient; public class BasketService : IBasketService
private readonly ILogger<BasketService> _logger;
public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger)
{ {
_basketClient = basketClient; private readonly Basket.BasketClient _basketClient;
_logger = logger; private readonly ILogger<BasketService> _logger;
}
public async Task<BasketData> GetByIdAsync(string id) public BasketService(Basket.BasketClient basketClient, ILogger<BasketService> logger)
{
_logger.LogDebug("grpc client created, request = {@id}", id);
var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
_logger.LogDebug("grpc response {@response}", response);
return MapToBasketData(response);
}
public async Task UpdateAsync(BasketData currentBasket)
{
_logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket);
var request = MapToCustomerBasketRequest(currentBasket);
_logger.LogDebug("Grpc update basket request {@request}", request);
await _basketClient.UpdateBasketAsync(request);
}
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
{
if (customerBasketRequest == null)
{ {
return null; _basketClient = basketClient;
_logger = logger;
} }
var map = new BasketData
{
BuyerId = customerBasketRequest.Buyerid
};
customerBasketRequest.Items.ToList().ForEach(item => public async Task<BasketData> GetById(string id)
{ {
if (item.Id != null) _logger.LogDebug("grpc client created, request = {@id}", id);
{ var response = await _basketClient.GetBasketByIdAsync(new BasketRequest { Id = id });
map.Items.Add(new BasketDataItem _logger.LogDebug("grpc response {@response}", response);
{
Id = item.Id,
OldUnitPrice = (decimal)item.Oldunitprice,
PictureUrl = item.Pictureurl,
ProductId = item.Productid,
ProductName = item.Productname,
Quantity = item.Quantity,
UnitPrice = (decimal)item.Unitprice
});
}
});
return map; return MapToBasketData(response);
}
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
{
if (basketData == null)
{
return null;
} }
var map = new CustomerBasketRequest public async Task UpdateAsync(BasketData currentBasket)
{ {
Buyerid = basketData.BuyerId _logger.LogDebug("Grpc update basket currentBasket {@currentBasket}", currentBasket);
}; var request = MapToCustomerBasketRequest(currentBasket);
_logger.LogDebug("Grpc update basket request {@request}", request);
basketData.Items.ToList().ForEach(item => await _basketClient.UpdateBasketAsync(request);
}
private BasketData MapToBasketData(CustomerBasketResponse customerBasketRequest)
{ {
if (item.Id != null) if (customerBasketRequest == null)
{ {
map.Items.Add(new BasketItemResponse return null;
{
Id = item.Id,
Oldunitprice = (double)item.OldUnitPrice,
Pictureurl = item.PictureUrl,
Productid = item.ProductId,
Productname = item.ProductName,
Quantity = item.Quantity,
Unitprice = (double)item.UnitPrice
});
} }
});
return map; var map = new BasketData
{
BuyerId = customerBasketRequest.Buyerid
};
customerBasketRequest.Items.ToList().ForEach(item =>
{
if (item.Id != null)
{
map.Items.Add(new BasketDataItem
{
Id = item.Id,
OldUnitPrice = (decimal)item.Oldunitprice,
PictureUrl = item.Pictureurl,
ProductId = item.Productid,
ProductName = item.Productname,
Quantity = item.Quantity,
UnitPrice = (decimal)item.Unitprice
});
}
});
return map;
}
private CustomerBasketRequest MapToCustomerBasketRequest(BasketData basketData)
{
if (basketData == null)
{
return null;
}
var map = new CustomerBasketRequest
{
Buyerid = basketData.BuyerId
};
basketData.Items.ToList().ForEach(item =>
{
if (item.Id != null)
{
map.Items.Add(new BasketItemResponse
{
Id = item.Id,
Oldunitprice = (double)item.OldUnitPrice,
Pictureurl = item.PictureUrl,
Productid = item.ProductId,
Productname = item.ProductName,
Quantity = item.Quantity,
Unitprice = (double)item.UnitPrice
});
}
});
return map;
}
} }
} }

View File

@ -1,44 +1,52 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using CatalogApi;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class CatalogService : ICatalogService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
private readonly Catalog.CatalogClient _client; public class CatalogService : ICatalogService
private readonly ILogger<CatalogService> _logger;
public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> logger)
{ {
_client = client; private readonly Catalog.CatalogClient _client;
_logger = logger; private readonly ILogger<CatalogService> _logger;
}
public async Task<CatalogItem> GetCatalogItemAsync(int id) public CatalogService(Catalog.CatalogClient client, ILogger<CatalogService> logger)
{
var request = new CatalogItemRequest { Id = id };
_logger.LogInformation("grpc request {@request}", request);
var response = await _client.GetItemByIdAsync(request);
_logger.LogInformation("grpc response {@response}", response);
return MapToCatalogItemResponse(response);
}
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
{
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 };
_logger.LogInformation("grpc request {@request}", request);
var response = await _client.GetItemsByIdsAsync(request);
_logger.LogInformation("grpc response {@response}", response);
return response.Data.Select(this.MapToCatalogItemResponse);
}
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse)
{
return new CatalogItem
{ {
Id = catalogItemResponse.Id, _client = client;
Name = catalogItemResponse.Name, _logger = logger;
PictureUri = catalogItemResponse.PictureUri, }
Price = (decimal)catalogItemResponse.Price
}; public async Task<CatalogItem> GetCatalogItemAsync(int id)
{
var request = new CatalogItemRequest { Id = id };
_logger.LogInformation("grpc request {@request}", request);
var response = await _client.GetItemByIdAsync(request);
_logger.LogInformation("grpc response {@response}", response);
return MapToCatalogItemResponse(response);
}
public async Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids)
{
var request = new CatalogItemsRequest { Ids = string.Join(",", ids), PageIndex = 1, PageSize = 10 };
_logger.LogInformation("grpc request {@request}", request);
var response = await _client.GetItemsByIdsAsync(request);
_logger.LogInformation("grpc response {@response}", response);
return response.Data.Select(this.MapToCatalogItemResponse);
}
private CatalogItem MapToCatalogItemResponse(CatalogItemResponse catalogItemResponse)
{
return new CatalogItem
{
Id = catalogItemResponse.Id,
Name = catalogItemResponse.Name,
PictureUri = catalogItemResponse.PictureUri,
Price = (decimal)catalogItemResponse.Price
};
}
} }
} }

View File

@ -1,8 +1,12 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IBasketService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
Task<BasketData> GetByIdAsync(string id); public interface IBasketService
{
Task<BasketData> GetById(string id);
Task UpdateAsync(BasketData currentBasket); Task UpdateAsync(BasketData currentBasket);
}
} }

View File

@ -1,8 +1,13 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
public interface ICatalogService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
Task<CatalogItem> GetCatalogItemAsync(int id); public interface ICatalogService
{
Task<CatalogItem> GetCatalogItemAsync(int id);
Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids); Task<IEnumerable<CatalogItem>> GetCatalogItemsAsync(IEnumerable<int> ids);
}
} }

View File

@ -1,6 +1,10 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IOrderApiClient namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket); public interface IOrderApiClient
{
Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket);
}
} }

View File

@ -1,6 +1,10 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using System.Threading.Tasks;
public interface IOrderingService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
Task<OrderData> GetOrderDraftAsync(BasketData basketData); public interface IOrderingService
} {
Task<OrderData> GetOrderDraftAsync(BasketData basketData);
}
}

View File

@ -1,28 +1,40 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
public class OrderApiClient : IOrderApiClient namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
private readonly HttpClient _apiClient; public class OrderApiClient : IOrderApiClient
private readonly ILogger<OrderApiClient> _logger;
private readonly UrlsConfig _urls;
public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config)
{ {
_apiClient = httpClient; private readonly HttpClient _apiClient;
_logger = logger; private readonly ILogger<OrderApiClient> _logger;
_urls = config.Value; private readonly UrlsConfig _urls;
}
public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket) public OrderApiClient(HttpClient httpClient, ILogger<OrderApiClient> logger, IOptions<UrlsConfig> config)
{ {
var url = $"{_urls.Orders}{UrlsConfig.OrdersOperations.GetOrderDraft()}"; _apiClient = httpClient;
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json"); _logger = logger;
var response = await _apiClient.PostAsync(url, content); _urls = config.Value;
}
response.EnsureSuccessStatusCode(); public async Task<OrderData> GetOrderDraftFromBasketAsync(BasketData basket)
{
var url = _urls.Orders + UrlsConfig.OrdersOperations.GetOrderDraft();
var content = new StringContent(JsonSerializer.Serialize(basket), System.Text.Encoding.UTF8, "application/json");
var response = await _apiClient.PostAsync(url, content);
var ordersDraftResponse = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, JsonDefaults.CaseInsensitiveOptions); var ordersDraftResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<OrderData>(ordersDraftResponse, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
} }
} }

View File

@ -1,72 +1,79 @@
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; using GrpcOrdering;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;
public class OrderingService : IOrderingService namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services
{ {
private readonly GrpcOrdering.OrderingGrpc.OrderingGrpcClient _orderingGrpcClient; public class OrderingService : IOrderingService
private readonly ILogger<OrderingService> _logger;
public OrderingService(GrpcOrdering.OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{ {
_orderingGrpcClient = orderingGrpcClient; private readonly OrderingGrpc.OrderingGrpcClient _orderingGrpcClient;
_logger = logger; private readonly ILogger<OrderingService> _logger;
}
public async Task<OrderData> GetOrderDraftAsync(BasketData basketData) public OrderingService(OrderingGrpc.OrderingGrpcClient orderingGrpcClient, ILogger<OrderingService> logger)
{
_logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
var command = MapToOrderDraftCommand(basketData);
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command);
_logger.LogDebug(" grpc response: {@response}", response);
return MapToResponse(response, basketData);
}
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData)
{
if (orderDraft == null)
{ {
return null; _orderingGrpcClient = orderingGrpcClient;
_logger = logger;
} }
var data = new OrderData public async Task<OrderData> GetOrderDraftAsync(BasketData basketData)
{ {
Buyer = basketData.BuyerId, _logger.LogDebug(" grpc client created, basketData={@basketData}", basketData);
Total = (decimal)orderDraft.Total,
};
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData var command = MapToOrderDraftCommand(basketData);
var response = await _orderingGrpcClient.CreateOrderDraftFromBasketDataAsync(command);
_logger.LogDebug(" grpc response: {@response}", response);
return MapToResponse(response, basketData);
}
private OrderData MapToResponse(GrpcOrdering.OrderDraftDTO orderDraft, BasketData basketData)
{ {
Discount = (decimal)o.Discount, if (orderDraft == null)
PictureUrl = o.PictureUrl, {
ProductId = o.ProductId, return null;
ProductName = o.ProductName, }
UnitPrice = (decimal)o.UnitPrice,
Units = o.Units, var data = new OrderData
})); {
Buyer = basketData.BuyerId,
Total = (decimal)orderDraft.Total,
};
orderDraft.OrderItems.ToList().ForEach(o => data.OrderItems.Add(new OrderItemData
{
Discount = (decimal)o.Discount,
PictureUrl = o.PictureUrl,
ProductId = o.ProductId,
ProductName = o.ProductName,
UnitPrice = (decimal)o.UnitPrice,
Units = o.Units,
}));
return data;
}
private CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{
var command = new CreateOrderDraftCommand
{
BuyerId = basketData.BuyerId,
};
basketData.Items.ForEach(i => command.Items.Add(new BasketItem
{
Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice,
PictureUrl = i.PictureUrl,
ProductId = i.ProductId,
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = (double)i.UnitPrice,
}));
return command;
}
return data;
} }
private GrpcOrdering.CreateOrderDraftCommand MapToOrderDraftCommand(BasketData basketData)
{
var command = new GrpcOrdering.CreateOrderDraftCommand
{
BuyerId = basketData.BuyerId,
};
basketData.Items.ForEach(i => command.Items.Add(new GrpcOrdering.BasketItem
{
Id = i.Id,
OldUnitPrice = (double)i.OldUnitPrice,
PictureUrl = i.PictureUrl,
ProductId = i.ProductId,
ProductName = i.ProductName,
Quantity = i.Quantity,
UnitPrice = (double)i.UnitPrice,
}));
return command;
}
} }

View File

@ -0,0 +1,225 @@
using CatalogApi;
using Devspaces.Support;
using GrpcBasket;
using GrpcOrdering;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure;
using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
namespace Microsoft.eShopOnContainers.Web.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)
.AddDevspaces()
.AddApplicationServices()
.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.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.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 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 = "webshoppingagg";
});
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.DescribeAllEnumsAsStrings();
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>()
.AddDevspacesSupport();
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,23 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net5.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>
<LangVersion>preview</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" /> <Folder Include="wwwroot\" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" />
<PackageReference Include="Grpc.Tools" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\Services\Services.Common\Services.Common.csproj" /> <PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="5.0.1" />
<PackageReference Include="Google.Protobuf" Version="3.14.0" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Core" Version="2.34.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.34.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.34.0" />
<PackageReference Include="Grpc.Tools" Version="2.34.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0-dev-00834" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<!--<PackageReference Include="System.Net.Http.WinHttpHandler" Version="4.6.0-rc1.19456.4" />-->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\Devspaces.Support\Devspaces.Support.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@ -1,138 +1,15 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "IncludeScopes": false,
"Default": "Information", "Debug": {
"Microsoft.AspNetCore": "Warning", "LogLevel": {
"System.Net.Http": "Warning" "Default": "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}"
}
} }
}, },
"Clusters": { "Console": {
"basket": { "LogLevel": {
"Destinations": { "Default": "Warning"
"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

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

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