diff --git a/README.md b/README.md index 7ab4a283..65fee2e7 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,18 @@ # SaaStack -Are you about to build a new SaaS product from scratch and do that on .NET? +Are you about to build a new SaaS product from scratch? On .NET? -Then, start with SaaStack. +Then, try starting with SaaStack codebase template. -It is a complete "codebase template" for building real-world, fully featured SaaS web products. +It is a complete template for building real-world, fully featured SaaS web products. Ready to build, test, and deploy into a cloud provider of your choice (e.g., Azure, AWS, Google Cloud, etc.) -> Don't spend months building all this stuff from scratch. You and your team don't need to. We've done all that for you already; just take a look, see hat is there and take it from here. You can always change it the way you like it as you proceed, you are not locked into anyone else framework. +> Don't spend months building all this stuff from scratch. You and your team don't need to. We've done all that for you already; just take a look, see what is already there and take it from here. You can always change it the way you like it as you proceed, you are not locked into anyone else's framework. > > This is not some code sample like those you would download to learn a new technology or see in demos online. This is way more comprehensive, way more contextualized, and way more realistic about the complexities you are going to encounter in reality. -> This template contains a partial (but fully functional) SaaS product that you can deploy from day one and start building your product on. But it is not yet complete. That part is up to you. +> This template contains a partial (but fully functional) SaaS product that you can deploy from day one and start building your product on. But it is not yet complete. That next part is up to you. The codebase demonstrates common architectural styles that you are going to need in your product in the long run, such as: @@ -42,7 +42,7 @@ or if you prefer AWS: ## Who is it for? -This starter template is NOT for everyone, nor for EVERY software project, nor for EVERY skill level. +This starter template is NOT for everyone, nor for EVERY software project, nor for EVERY skill level. We need to say that because all software products are different, there is not one silver bullet for all of them. * The people using this template must have some experience applying "first principles" of building new software products from scratch because it is a starter template that can (and should) be modified to suit your context. It is a far better starting point than building everything from scratch again. @@ -126,7 +126,7 @@ The starter template also takes care of these specific kinds of things: * It integrates product usage metrics to monitor and measure the actual usage of your product (e.g., MixPanel, Google Analytics, Application Insights, Amazon XRay, etc.) * It integrates crash analytics and structured logging so you can plug in your own preferred monitoring (e.g., Application Insights, CloudWatch, Sentry.io, etc.). * It uses dependency injection extensively so that all modules and components remain testable and configurable. - * It defines standard and unified configuration patterns (e.g., using appsettings.json) to load tenanted or non-tenanted runtime settings. + * It defines standard and unified configuration patterns (e.g., using `appsettings.json`) to load tenanted or non-tenanted runtime settings. * Application * Supports one or more applications, agnostic to infrastructure interop (i.e., allows you to expose each application as a REST API (default) or as a reliable Queue, or any other kind of infrastructure) * Supports transaction scripts + anemic domain model or Domain Driven Design diff --git a/docs/design-principles/0080-ports-and-adapters.md b/docs/design-principles/0080-ports-and-adapters.md index 20573ddd..01de74a7 100644 --- a/docs/design-principles/0080-ports-and-adapters.md +++ b/docs/design-principles/0080-ports-and-adapters.md @@ -1,21 +1,29 @@ # Ports and Adapters -The underlying architectural style in this codebase is the Ports and Adapters. This means that the application is designed to be driven by the domain model, and the domain model is not dependent on any external infrastructure. +The underlying architectural style in this codebase is the Ports and Adapters. This means that the application is designed to drive and be driven by the domain model, and the domain model is not dependent on any external infrastructure. + +![Ports and Adapters](../images/Ports-And-Adapters.png) + +In this way, we can re-focus on the core domain model and be protected by changes in infrastructure. + +Possibly the most powerful effect of this approach is that any data, any service, and interaction that is needed by the core domain is accessible by a domain specific "port" and can be implemented by one or more "adapters" connecting to other components or external systems. For the developer, this liberates them to tackle a complex interaction in testable steps. This is achieved by using the [Adapter Pattern](https://en.wikipedia.org/wiki/Adapter_pattern) to connect the domain model to the external infrastructure. -In this general de-coupling pattern can also be applied in any layer of the code, but it is particularly useful in the "Application Layer", and in the "Infrastructure Layers". The pattern can also be cascaded between components to offer several abstraction layers. +In general, this de-coupling pattern can also be applied in any layer of the code, but it is particularly useful in the "Application Layer" and in the "Infrastructure Layers". The pattern can also be used in composing infrastructure components together to offer several abstraction layers, between the logical and physical layers. + +> In other words, you don't need to go from a single "port" to a single "adapter" representing physical infrastructure (like a database). Your "port" can be implemented by an "adapter" that exposes a lower level "port" that can be implemented by a lower level "adapter". A "port" defines the data and behavior required by the consumer of it. Thus the consumer is always protected from changes in any implementation of the "port". > This pattern is also known as the "[Plug-in Pattern](https://en.wikipedia.org/wiki/Plug-in_(computing))", which is common in reusable libraries or frameworks that offer extensibility points for those using them. -A "port", in code, is simply defined in an interface. +A "port," in code is simply defined in an interface. > It is designed to be as small as possible, and as specific as possible, adhering to the [ISP principle](https://en.wikipedia.org/wiki/SOLID). A "service" that implements the "port", is known as an "adapter" An "adapter" is realized as a concrete class, that simply implements the "port" interface. -> A adapter, in code, rarely ever uses the term "adapter" as part of its name or identifier. Rather, it is usually named after the behavior it provides, or the 3rd party library it wraps, or the 3rd party service it integrates with. +> An adapter, in code, rarely ever uses the term "adapter" as part of its name or identifier. Rather, it is usually named after the behavior it provides, the 3rd party library it wraps, or the 3rd party service it integrates with. Whenever a port is required to be used by a component, it declares the port in its constructor, and dependency injection is used to inject a real adapter into the component at runtime (depending on what is currently registered in the container at that time). @@ -26,33 +34,41 @@ This concrete "adapter" is then registered in the dependency injection container What this also enables is that the code for the port and code for the adapter(s) can be kept in different libraries, further reducing the coupling of code in reusable libraries in the code. This leads to far better maintainability and portability of shared libraries in the code. Which is a key enabler for modular monoliths, when they have to be split up. -![Ports and Adapters](../images/Ports-And-Adapters.png) - ## Design Principles * We want to maintain high levels of de-coupling for performing any actions outside subdomain aggregates in the "Domain Layer". This means that access to any data outside of a subdomain requires the use of a "port" to obtain it, which provides data that can be presented to the aggregate in the subdomain. This is the primary job of the "Application Layer". -* We want to be able to swap out the implementation of any "adapter" (something that provides/processes it own data), without changing the domain model, or requiring a change in it. -* We want to be able to unit test the domain model in isolation without requiring any external infrastructure.(A domain model should only require data, and in some rare cases require "domain services" which can be mocked in order to be tested). +* We want to be able to swap out the implementation of any "adapter" (something that provides/processes its own data) without changing the domain model or requiring a change in it. +* We want to be able to unit test the domain model in isolation without requiring any external infrastructure. (A domain model should only require data and, in some rare cases, require "domain services" which can be mocked in order to be tested). * We want to be able to test the real infrastructural "adapters" in isolation also, without needing to use the domain model. -* If the real infrastructure "adapters" requires external 3rd party services to run properly, then we want to be able to test them in isolation as well. (using testing-only configurations) -* Given that we can reliably unit test the domain model independently, and we can reliably unit test the infrastructure components independently, and given that we can integration test the domain model with "stubbed" infrastructure "adapters" (that can "fake" the behavior of real infrastructure "adapter"), then we can then be very confident that the system will work as expected when it is deployed with real infrastructure "adapters" in staging/production environments. In fact, if this is the case, then the only unknown should be whether the staging/production configuration used by the real infrastructure "adapter" is valid or not in the staging/production environments. If it is valid, then the system should work ain staging/production as designed. +* If the real infrastructure "adapters" require external 3rd party services to run properly, then we want to be able to test them in isolation as well. (using testing-only configurations) +* Given that we can reliably unit test the domain model independently, and we can reliably unit test the infrastructure components independently and given that we can integration test the domain model with "stubbed" infrastructure "adapters" (that can "fake" the behavior of real infrastructure "adapter"), then we can then be very confident that the system will work as expected when it is deployed with real infrastructure "adapters" in staging/production environments. In fact, if this is the case, then the only unknown should be whether the staging/production configuration used by the real infrastructure "adapter" is valid or not in the staging/production environments. If it is valid, then the system should work in staging/production as designed. * When we run the code locally in the local environment (for manual testing or demos, etc.) we want to avoid having to install any infrastructural components that cannot be swapped out for adapters that run in memory or on disk on the local machine. For example, we don't want to install databases, or data servers of any kind. -* When we run the code in automated testing we also do not to have to install any infrastructural components, especially if they require network communication. Local automated testing and automated testing in CI should be offline, and trivial to configure. +* When we run the code in automated testing we also do not have to install any infrastructural components, especially if they require network communication. Local automated testing and automated testing in CI should be offline, and trivial to configure. ## Implementation -TBD +### Application Services -### Adapters to 3rd Party Services +Many of the ports and adapters in the codebase (not exclusively) are to be found in the "services" used by the classes in the "Application Layer". + +> They are often known more commonly as "services" borrowed from other, more traditional architectures. + +The classes in the "Application Layer" of any subdomain (e.g., `CarsApplication`) define new "ports" (e.g., `IFeatureFlags`) to either access data from other components or to delegate certain operations to other components. This is also a way to communicate with other subdomains. + +Typically, the "Application Layer" classes consume (or drive) these "ports". This is an important mechanism to separate concerns of the classes in the application layer, whose primary purpose is to obtain data for the aggregate of the "Domain Layer" to operate. + +> Incidentally, the classes in the "Application Layer" of a given module (e.g., `CarsApplication`) are, in fact, "adapters" themselves. They implement the declared "port" (e.g. `ICarsApplication`). It is the upstream API class that consumes (or drives) this "port". + +### 3rd Party Services Adapters to 3rd party remote systems (a.k.a "3rd party integrations") are a special kind of "adapter" requiring additional and special treatment in the codebase, so that they are properly tested and configured correctly for use in local environments, as well as in staging/production environments. -The main difference with them is that they: +The main difference between them is that they: -1. Typically require the creation of accounts in the 3rd party online system (e.g. register an account with a 3rd party service, and obtain an API key, or a client ID, or a client secret, etc). In these cases, they require a separate accounts for testingonly and separate accounts for staging/production use. Testingonly accounts can never lead to compromising staging/production accounts. Some 3rd party providers mitigate this with sandbox environments, but not all do. -2. They often offer the use of a 3rd party SDK, or a 3rd party library, or a 3rd party service, to communicate with the 3rd party system. These can be very useful sometimes, dependencing on their sophistication (particularly with caching, retry policies etc), but they must be configurable to point to local "stubbed" versions of them in order to be used in local development environments. -3. They require connection configuration (and often secrets) to access the 3rd party system, which is often different between local, staging, and production environments. Details for staging/production environments can never be hard-coded into the codebase, and must be configurable at deployment time. -4. They require us to build one or more "stub" implementations of the 3rd party system, so that they can be used in local development and in automated testing, without requiring access to the real 3rd party system. +1. Typically require the creation of accounts in the 3rd party online system (e.g., register an account with a 3rd party service and obtain an API key, a client ID, a client secret, etc). In these cases, they require separate accounts for testing-only and separate accounts for staging/production use. Testing-only accounts can never lead observers to compromising staging/production accounts. Some 3rd party providers mitigate this with sandbox environments, but not all provide that service. +2. They often offer the use of a 3rd party SDK, a 3rd party library, or a 3rd party service to communicate with the 3rd party system. These can be very useful sometimes, depending on their sophistication (particularly with caching, retry policies, etc), but they must be configurable to point to local "stubbed" versions of them in order to be used in local development environments. +3. They require connection configuration (and often secrets) to access the 3rd party system, which often differs between local, staging, and production environments. Details for staging/production environments can never be hard-coded into the codebase, and must be configurable at deployment time. +4. They require us to build one or more "stub" implementations of the 3rd party system so that they can be used in local development and in automated testing without requiring access to the real 3rd party system. In practice, to build one of these 3rd party adapters, you need to take extra time and care to provide (at least) these six things: @@ -66,16 +82,16 @@ In practice, to build one of these 3rd party adapters, you need to take extra ti ### Adapter Most external adapters for 3rd party integrations, that you build, should be built and maintained in the `Infrastructure.Shared` project in a folder called: `ApplicationServices/External`. -> This single project would be in the only project that would take a dependency on any 3rd party nuget packages required by these adapters, which should be kept to a minimum. +> This single project would be the only project that would take a dependency on any 3rd party NuGet packages required by these adapters, which should be kept to a minimum. -Your adapter will likely act as a HTTPS "ServiceClient" to a remote 3rd party service (in the cloud, or deployed in your cloud a.k.a on-premise). Thus, the name of your adapter is likely follow the naming pattern: `VendorHttpServiceClient`. +Your adapter will likely act as an HTTPS "Service Client" to a remote 3rd party service (in the cloud or deployed in your cloud, a.k.a on-premise). Thus, the name of your adapter is likely to follow the naming pattern: `VendorHttpServiceClient`. -Your adapter can use the vendor provided SDK library (from nuget), but consider these challenges: +Your adapter can use the vendor-provided SDK library (from NuGet), but consider these challenges: -1. If the adapter has significant complexity to it, and/or your adapter has behavior that you feel should be unit tested, then the use of the SDK library will make it very hard to unit test the adapter, since you cannot be accessing a remote system during unit testing. -2. If this adapter is going to used in local development (i.e. it is injected into the DI container in local development), then the SDK will be required to configured to direct all its HTTP traffic to a stub API that you must provide (refer to step 6 above). +1. If the adapter has significant complexity to it, and/or your adapter has behavior that you feel should be unit tested, then using the SDK library will make it very hard to unit test the adapter since you cannot be accessing a remote system during unit testing. +2. If this adapter is going to be used in local development (i.e., it is injected into the DI container in local development), then the SDK will be required to be configured to direct all its HTTP traffic to a stub API that you must provide (refer to step 6 above). -If you want to unit test your adapter (not always necessary), then an easy technique to use is to wrap the SDK code inside another port (e.g. `IVendorServiceClient`), and then implement that adapter using the vendor provided SDK, with a internal constructor used only for testing. This will allow you to unit test your original adapter, by injecting a `new Mock()`. +If you want to unit test your adapter (not always necessary), then an easy technique to use is to wrap the SDK code inside another port (e.g. , `IVendorServiceClient`) and then implement that adapter using the vendor-provided SDK, with an internal constructor used only for testing. This will allow you to unit test your original adapter, by injecting a `new Mock()`. For example, @@ -99,20 +115,22 @@ public class VendorHttpServiceClient : IMyPort } ``` -If you are plugging in your adapter to be used in local development and automated testing, then you will be providing a stub API (as per step 6 above). In this case, the vendor SDK will need to be able to send its HTTP requests to another URL other than the one in the cloud where their service is hosted (e.g. `https://api.vendor.com`). The SDk must support a way for you to change that `baseUrl`. If not, you have few great choices other than to: +If you are plugging in your adapter for local development and automated testing, then you will be providing a stub API (as per step 6 above). In this case, the vendor SDK will need to be able to send its HTTP requests to another URL other than the one in the cloud where their service is hosted (e.g., `https://api.vendor.com`). The SDK must support a way for you to change that `BaseUrl`. + +If not, you have very few good choices other than to: 1. Inject a different implementation of the `IVendorServiceClient` above. 2. Forgo using the vendor SDK altogether, and instead use the `ApiServiceClient` and send the HTTP requests yourself to the remote vendor API. This is more work, but it is the only way to ensure that the adapter can be used in local development and automated testing properly. ### Accounts with 3rd party -In general, most 3rd party vendor provided services will require you to register and create accounts with them to gain access to their public APIs. +In general, most 3rd party vendor-provided services will require you to register and create accounts with them to gain access to their public APIs. > Note: some of the services require being paid for, which is another hurdle. -For most of these integrations you are going to need to create at least two accounts with the 3rd party: +For most of these integrations, you are going to need to create at least two accounts with the 3rd party: -1. One account called "testingonly" that is used for local development and automated testing. -2. One/more account(s) for "staging/production" that are used for staging and production environments. +1. One account called "testingonly" that will be used for local development and automated testing. +2. One/more account(s) for "staging/production" that is used for staging and production environments. > Some vendors provide sandboxes for testing, which can be used in place of the "testingonly" account. This is a great feature, but not all vendors provide this. @@ -120,9 +138,9 @@ Regardless, the credentials, access, and configurations between these accounts m You must never expose staging/production credentials or configuration outside your organization. This is a serious security risk. -You can however, expose `testingonly` credentials and configuration inside your organization, but only if they cannot lead to those used in staging/production environments. Separate secured and protected accounts are strongly recommended. +You can, however, expose testing-only credentials and configuration inside your organization, but only if they cannot lead to those used in staging/production environments. Separate secured and protected accounts are strongly recommended. -> In this SaaStack codebase template, the contributors have registered `testingonly` accounts with various vendors, and we have hard-coded some of those credentials the various `appsettings.Testing.json` files, of the various adapter integration testing projects, in the codebase. This is a security risk, but it is acceptable for the purposes of this template, since this cannot lead to any staging/production environments. These accounts can be compromised with little exposure. In a real derivative project that you are using, if this code was exposed outside your organization (for example in open source) you should not do this. If the codebase is not exposed outside your own organization, there is some risk also in exposing these to those with access to your code. You might consider using secrets files, or environment variables, or a secret manager, or a key vault, or a configuration service, to manage this sensitive configuration. +> In this SaaStack codebase template, the contributors have registered testing-only accounts with various vendors, and we have hard-coded some of those credentials in the various `appsettings.Testing.json` files of the various adapter integration testing projects, in the codebase. This is a security risk, but it is acceptable for the purposes of this template since this cannot lead to any staging/production environments. These accounts can be compromised with little exposure. In a real derivative project, you are using, if this code was exposed outside your organization (for example, in open source), you should not do this. If the codebase is not exposed outside your own organization, there is also some risk in exposing these to those with access to your code. You might consider using secrets files, environment variables, a secret manager, a key vault, or a configuration service to manage this sensitive configuration. ### Integration Tests @@ -131,17 +149,17 @@ Integration tests are REQUIRED to test the real adapter against the real 3rd par These integration tests are different than others, in many ways: 1. They are of a different category called "Integration.External" -2. They should not be executed frequently by the team, like other integration tests are, since they are testing against real 3rd party systems, and can be slow, and can be rate limited, and can incur costs to your organization (depending on the 3rd party system, and pricing). -3. They should be executed infrequently in CI builds, perhaps once a week? or whenever the code in the adapters is changed. -4. They can fail for time to time, depending on how well managed the 3rd party vendor is at maintaining their systems. When these tests do break, its a pretty clear indicator that the 3rd party service has changed from what it used to be, and your team needs to know ASAP. Or your adapters are now broken, and need to be fixed. +2. They should not be executed frequently by the team, like other integration tests are, since they are testing against real 3rd party systems, can be slow, can be rate limited, and can incur costs to your organization (depending on the 3rd party system, and pricing). +3. They should be executed infrequently in CI builds, perhaps once a week/month or whenever the code in the adapters is changed. +4. They can fail from time to time, depending on how well-managed the 3rd party vendor is at maintaining their systems. When these tests do break, it's a pretty clear indicator that the 3rd party service has changed from what it used to be, and your team needs to know ASAP. Or perhaps your adapters are now broken and need to be fixed. ### Configuration The adapter must be configurable at runtime, so that it can be used in local development, automated testing, and in staging/production environments. -This configuration is likely to be kept in `appsettings.json` files in each of the host projects that use the adapter (e.g. `ApiHost1.csproj`). +This configuration is likely to be kept in `appsettings.json` files in each of the host projects that use the adapter (e.g., `ApiHost1.csproj`). -Configuration for each of these adapter is done under the `ApplicationServices` section of the `appsettings.json` file, usually under a key named after the vendor. +Configuration for each of these adapters is done under the `ApplicationServices` section of the `appsettings.json` file, usually under a key named after the vendor. For example, @@ -155,19 +173,19 @@ For example, } } } -``` + ``` -Now, from a security perspective and a testing perspective, you do not want to define any real configuration settings here, since that would expose them to anyone that can see the code. +Now, from a security perspective and a testing perspective, you do not want to define any real configuration settings here since that would expose them to anyone who can see the code. In automated "External" Integration testing (as described above), the configuration used there to talk to a real online service, is kept in `appsettings.Testing.json` (and in `appsettings.testing.local.json`) in the testing project. -In automated "API" Integration testing, your adapter is going to be swapped out for a "stubbed" adapter that is injected at testing time, using overrides in the integration testing project. So that your adapter is not used at all. +In automated "API" Integration testing, your adapter is going to be swapped out for a "stubbed" adapter that is injected at testing time, using overrides in the integration testing project so that your adapter is not used at all. Configuration in the host project is there for running the adapter in local development and in staging/production environments. -For staging/production environments, your automated deployment process should be substituting/replacing the configuration in `appsettings.json` just before deployment time, with correct configuration to real 3rd party systems. +For staging/production environments, your automated deployment process should be substituting/replacing the configuration in `appsettings.json` just before deployment time with the correct configuration to real 3rd party systems. -But for local development (manual debugging and testing), you need settings here that can be used in your adapter, to talk to a stub API (see below). This means that you want predominantly empty placeholder or testing entries in `appsettings.json`, that are no sensitive values. +But for local development (manual debugging and testing), you need settings here that can be used in your adapter, to talk to a stub API (see below). This means that you want predominantly empty placeholder or testing entries in `appsettings.json` that have no sensitive values. > Remember: to update all staging/production configurations to your CI/CD systems so that you don't forget those settings on the next deployment of code. @@ -180,13 +198,13 @@ The adapter must be registered in the dependency injection container, so that it Furthermore, you can use compiler compilation directives to register this adapter in different build configurations or different hosting environments. -For example, if you want it to only be used in local development and automated testing, or never in either of those, you can use the `#if TESTINGONLY` directive. +For example, if you want it only to be used in local development and automated testing, or never in either of those, you can use the `#if TESTINGONLY` directive. -For example, if you want it to only be used in an AZURE deployment, not an AWS deployment, or never in either of those, you can use the `#if HOSTEDONAZURE || HOSTEDONAWS` directives. +For example, if you want it only to be used in an AZURE deployment, not an AWS deployment, or never in either of those, you can use the `#if HOSTEDONAZURE || HOSTEDONAWS` directives. ### Stub Adapter -In automated "API" Integration testing, we do NOT want to be using the real adapter at all, instead, we want to use a programmable "stub", so we can control its behaviour and we don't want to use online access to real systems across the network. +In automated "API" Integration testing, we do NOT want to be using the real adapter at all. Instead, we want to use a programmable "stub," so we can control its behavior, and we don't want to use online access to real systems across the network. Depending on which subdomain the adapter is used in, you will need to provide a stub adapter that can be used in automated "API" Integration testing, rather than using the real adapter at all. @@ -194,8 +212,8 @@ This "stub" adapter replaces your adapter, and provides a fake implementation of You have two choices depending on the scope of your adapter. -1. If your adapter is used by all subdomains and all hosts (i.e. it is used everywhere), then you can create a "stub" adapter in the `IntegrationTesting.Web.Api.Common` project in the `Stubs` folder, and you can inject it into the `WebApiSetup` in the `ConfigureWebHost` method along with the other global adapters. -2. If you adapter is only used in one (or more) specific subdomains, then you can create the "stub" adapter in the integration testing project of that subdomain (i.e. the `SubdomainInfrastructure.IntegrationTests` project), in a folder called `Stubs`, and you can inject that sub into the `OverrideDependencies` method of your test class. +1. If your adapter is used by all subdomains and all hosts (i.e., it is used everywhere), then you can create a "stub" adapter in the `IntegrationTesting.Web.Api.Common` project in the `Stubs` folder, and you can inject it into the `WebApiSetup` in the `ConfigureWebHost` method along with the other global adapters. +2. If your adapter is only used in one (or more) specific subdomains, then you can create the "stub" adapter in the integration testing project of that subdomain (i.e., the `SubdomainInfrastructure.IntegrationTests` project), in a folder called `Stubs`, and you can inject that sub into the `OverrideDependencies` method of your test class. For example, if you take a look at the `StubFeatureFlags` in the `IntegrationTesting.Web.Api.Common` project, you can see how it is injected into the `WebApiSetup` in the `ConfigureWebHost` method. @@ -203,7 +221,7 @@ For example, if you look at the `StubEmailDeliveryService` in the `AncillaryInfr ### Stub API -In local development, we DO want to be using the real adapter, but we want to have a programmable 3rd party "stub" API, so we can control its behaviour and we don't want to use online access to real systems across the network. +In local development, we DO want to be using the real adapter, but we want to have a programmable 3rd party "stub" API so that we can control its behavior, and we don't want to use online access to real systems across the network. This "stub" API stands in for the real 3rd party API, and the real adapter communicates with this "stub" API. @@ -225,10 +243,10 @@ For example, the `BaseUrl` configuration setting in the `appsettings.json` file In the `TestingStubApiHost` project, in the `Api` folder: -1. Create a class that derives from `StubApiBase` (e.g. `StubVendorApi`). +1. Create a class that derives from `StubApiBase` (e.g., `StubVendorApi`). 2. Consider applying a `WebServiceAttribute` to define a prefix for this API (e.g. `[WebService("/vendor")]`) to separate this API from the other vendors that will also be hosted at `https://localhost:5656`. 3. Implement the HTTP endpoints that your adapter uses, and provide empty or default responses that your adapter expects to receive. (same as the way we implement any endpoints) -4. The Request types, Response types (and all complex data types that are used in the request and response) should all be defined in the `Infrastructure.Web.Api.Operations.Shared` project in a subfolder of the `3rdParties` folder named after the vendor (i.e. `3rdParties/Vendor`). These types follow the same patterns as requests and responses for all other API operations in the codebase. Except that these ones may use additional JSON attributes to match the real 3rd party APIs. (e.g. `JsonPropertyName` and `JsonConverter` attributes). +4. The Request types, Response types (and all complex data types that are used in the request and response) should all be defined in the `Infrastructure.Web.Api.Operations.Shared` project in a subfolder of the `3rdParties` folder named after the vendor (i.e., `3rdParties/Vendor`). These types follow the same patterns as requests and responses for all other API operations in the codebase. Except that these ones may use additional JSON attributes to match the real 3rd party APIs. (e.g., `JsonPropertyName` and `JsonConverter` attributes). 5. Make sure that you trace out (using the `IRecorder`) each and every request to your Stub API (follow other examples) so that you can visually track when the API is called (by your adapter) in local testing. You can see this output in the console output for the `TestingStubApiHost` project. For example,