diff --git a/docs/Database-Cdc-Config-Xml.md b/docs/Database-Cdc-Config-Xml.md
index e99c7e603..98bf2eaab 100644
--- a/docs/Database-Cdc-Config-Xml.md
+++ b/docs/Database-Cdc-Config-Xml.md
@@ -63,7 +63,8 @@ Property | Description
`DatabaseName` | The .NET database interface name. Defaults to `IDatabase`.
`EventSubject` | The Event Subject. Defaults to `ModelName`. Note: when used in code-generation the `CodeGeneration.EventSubjectRoot` will be prepended where specified.
`IncludeColumnsOnDelete` | The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
-**`ExcludeBackgroundService`** | The option to exclude the generation of the `BackgroundService` class (`XxxBackgroundService.cs`). Valid options are: `No`, `Yes`.
+**`ExcludeHostedService`** | The option to exclude the generation of the `CdcHostedService` (background) class (`XxxHostedService.cs`). Valid options are: `No`, `Yes`.
+`ExcludeColumnsFromETag` | The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
diff --git a/docs/Database-Cdc-Config.md b/docs/Database-Cdc-Config.md
index 054618377..6772c9990 100644
--- a/docs/Database-Cdc-Config.md
+++ b/docs/Database-Cdc-Config.md
@@ -71,7 +71,8 @@ Property | Description
`databaseName` | The .NET database interface name. Defaults to `IDatabase`.
`eventSubject` | The Event Subject. Defaults to `ModelName`. Note: when used in code-generation the `CodeGeneration.EventSubjectRoot` will be prepended where specified.
`includeColumnsOnDelete` | The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
-**`excludeBackgroundService`** | The option to exclude the generation of the `BackgroundService` class (`XxxBackgroundService.cs`). Valid options are: `No`, `Yes`.
+**`excludeHostedService`** | The option to exclude the generation of the `CdcHostedService` (background) class (`XxxHostedService.cs`). Valid options are: `No`, `Yes`.
+`excludeColumnsFromETag` | The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
diff --git a/docs/Database-CdcJoin-Config-Xml.md b/docs/Database-CdcJoin-Config-Xml.md
index 451a909da..79ed36678 100644
--- a/docs/Database-CdcJoin-Config-Xml.md
+++ b/docs/Database-CdcJoin-Config-Xml.md
@@ -70,6 +70,7 @@ Property | Description
`ModelName` | The .NET model name. Defaults to `Name`.
`PropertyName` | The .NET property name. Defaults to `TableName` where `JoinCardinality` is `OneToOne`; otherwise, it will be `Name` suffixed by an `s` except when already ending in `s` where it will be suffixed by an `es`.
`IncludeColumnsOnDelete` | The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
+`ExcludeColumnsFromETag` | The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
diff --git a/docs/Database-CdcJoin-Config.md b/docs/Database-CdcJoin-Config.md
index 95e2c201e..b7c040c28 100644
--- a/docs/Database-CdcJoin-Config.md
+++ b/docs/Database-CdcJoin-Config.md
@@ -78,6 +78,7 @@ Property | Description
`modelName` | The .NET model name. Defaults to `Name`.
`propertyName` | The .NET property name. Defaults to `TableName` where `JoinCardinality` is `OneToOne`; otherwise, it will be `Name` suffixed by an `s` except when already ending in `s` where it will be suffixed by an `es`.
`includeColumnsOnDelete` | The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
+`excludeColumnsFromETag` | The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
diff --git a/docs/Database-CodeGeneration-Config-Xml.md b/docs/Database-CodeGeneration-Config-Xml.md
index d38a4db57..c2f3e42af 100644
--- a/docs/Database-CodeGeneration-Config-Xml.md
+++ b/docs/Database-CodeGeneration-Config-Xml.md
@@ -49,8 +49,10 @@ Property | Description
`CdcIdentifierMapping` | Indicates whether to include the generation of the generic `Cdc`-IdentifierMapping database capabilities.
`CdcIdentifierMappingTableName` | The table name for the `Cdc`-IdentifierMapping. Defaults to `CdcIdentifierMapping` (literal).
`CdcIdentifierMappingStoredProcedureName` | The table name for the `Cdc`-IdentifierMapping. Defaults to `spCreateCdcIdentifierMapping` (literal).
-**`EventSubjectRoot`** | The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
-**`EventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `UpperCase`, `PastTense`, `PastTenseUpperCase`. Defaults to `None` (no formatting required).
+`EventSourceRoot` | The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+`EventSourceKind` | The URI kind for the event source URIs. Valid options are: `None`, `Absolute`, `Relative`, `RelativeOrAbsolute`. Defaults to `None` (being the event source is not updated).
+**`EventSubjectRoot`** | The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be extended within the `Entity`(s).
+**`EventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `PastTense`. Defaults to `None` (no formatting required, i.e. as-is).
`JsonSerializer` | The JSON Serializer to use for JSON property attribution. Valid options are: `None`, `Newtonsoft`. Defaults to `Newtonsoft`. This can be overridden within the `Entity`(s).
`PluralizeCollectionProperties` | Indicates whether the .NET collection properties should be pluralized.
`HasBeefDbo` | Indicates whether the database has (contains) the standard _Beef_ `dbo` schema objects. Defaults to `true`.
@@ -75,6 +77,7 @@ Provides the _.NET_ configuration.
Property | Description
-|-
+`CdcExcludeColumnsFromETag` | The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)
`AutoDotNetRename` | The option to automatically rename the SQL Tables and Columns for use in .NET. Valid options are: `None`, `PascalCase`, `SnakeKebabToPascalCase`. Defaults to `PascalCase` which will capatilize the first character. The `SnakeKebabToPascalCase` option will remove any underscores or hyphens separating each word and capitalize the first character of each; e.g. `internal-customer_id` would be renamed as `InternalCustomerId`.
diff --git a/docs/Database-CodeGeneration-Config.md b/docs/Database-CodeGeneration-Config.md
index de2477e82..cec7bbd4b 100644
--- a/docs/Database-CodeGeneration-Config.md
+++ b/docs/Database-CodeGeneration-Config.md
@@ -49,8 +49,10 @@ Property | Description
`cdcIdentifierMapping` | Indicates whether to include the generation of the generic `Cdc`-IdentifierMapping database capabilities.
`cdcIdentifierMappingTableName` | The table name for the `Cdc`-IdentifierMapping. Defaults to `CdcIdentifierMapping` (literal).
`cdcIdentifierMappingStoredProcedureName` | The table name for the `Cdc`-IdentifierMapping. Defaults to `spCreateCdcIdentifierMapping` (literal).
-**`eventSubjectRoot`** | The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
-**`eventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `UpperCase`, `PastTense`, `PastTenseUpperCase`. Defaults to `None` (no formatting required).
+`eventSourceRoot` | The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+`eventSourceKind` | The URI kind for the event source URIs. Valid options are: `None`, `Absolute`, `Relative`, `RelativeOrAbsolute`. Defaults to `None` (being the event source is not updated).
+**`eventSubjectRoot`** | The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be extended within the `Entity`(s).
+**`eventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `PastTense`. Defaults to `None` (no formatting required, i.e. as-is).
`jsonSerializer` | The JSON Serializer to use for JSON property attribution. Valid options are: `None`, `Newtonsoft`. Defaults to `Newtonsoft`. This can be overridden within the `Entity`(s).
`pluralizeCollectionProperties` | Indicates whether the .NET collection properties should be pluralized.
`hasBeefDbo` | Indicates whether the database has (contains) the standard _Beef_ `dbo` schema objects. Defaults to `true`.
@@ -75,6 +77,7 @@ Provides the _.NET_ configuration.
Property | Description
-|-
+`cdcExcludeColumnsFromETag` | The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)
`autoDotNetRename` | The option to automatically rename the SQL Tables and Columns for use in .NET. Valid options are: `None`, `PascalCase`, `SnakeKebabToPascalCase`. Defaults to `PascalCase` which will capatilize the first character. The `SnakeKebabToPascalCase` option will remove any underscores or hyphens separating each word and capitalize the first character of each; e.g. `internal-customer_id` would be renamed as `InternalCustomerId`.
diff --git a/docs/Entity-CodeGeneration-Config-Xml.md b/docs/Entity-CodeGeneration-Config-Xml.md
index 9422b34fb..7ca5ea87c 100644
--- a/docs/Entity-CodeGeneration-Config-Xml.md
+++ b/docs/Entity-CodeGeneration-Config-Xml.md
@@ -80,9 +80,12 @@ Provides the _Data Services-layer_ configuration.
Property | Description
-|-
**`EventPublish`** | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to `true`. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
+`EventSourceRoot` | The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+`EventSourceKind` | The URI kind for the event source URIs. Valid options are: `None`, `Absolute`, `Relative`, `RelativeOrAbsolute`. Defaults to `None` (being the event source is not updated).
**`EventSubjectRoot`** | The root for the event Subject name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
`EventSubjectFormat` | The default formatting for the Subject when an Event is published. Valid options are: `NameOnly`, `NameAndKey`. Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.
-**`EventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `UpperCase`, `PastTense`, `PastTenseUpperCase`. Defaults to `None` (no formatting required)`.
+`EventSubjectSeparator` | The subject path separator. Defaults to `.`. Used only where the subject is automatically inferred.
+**`EventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `PastTense`. Defaults to `None` (no formatting required, i.e. as-is)`.
**`EventTransaction`** | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer whereever generating event publishing logic. Usage will force a rollback of any underlying data transaction (where the provider supports TransactionScope) on failure, such as an `EventPublish` error. This is by no means implying a Distributed Transaction (DTC) should be invoked; this is only intended for a single data source that supports a TransactionScope to guarantee reliable event publishing. Defaults to `false`. This essentially defaults the `Entity.EventTransaction` where not otherwise specified.
diff --git a/docs/Entity-CodeGeneration-Config.md b/docs/Entity-CodeGeneration-Config.md
index dc2079758..d50efebd9 100644
--- a/docs/Entity-CodeGeneration-Config.md
+++ b/docs/Entity-CodeGeneration-Config.md
@@ -93,9 +93,12 @@ Provides the _Data Services-layer_ configuration.
Property | Description
-|-
**`eventPublish`** | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to `true`. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
+`eventSourceRoot` | The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+`eventSourceKind` | The URI kind for the event source URIs. Valid options are: `None`, `Absolute`, `Relative`, `RelativeOrAbsolute`. Defaults to `None` (being the event source is not updated).
**`eventSubjectRoot`** | The root for the event Subject name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
`eventSubjectFormat` | The default formatting for the Subject when an Event is published. Valid options are: `NameOnly`, `NameAndKey`. Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.
-**`eventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `UpperCase`, `PastTense`, `PastTenseUpperCase`. Defaults to `None` (no formatting required)`.
+`eventSubjectSeparator` | The subject path separator. Defaults to `.`. Used only where the subject is automatically inferred.
+**`eventActionFormat`** | The formatting for the Action when an Event is published. Valid options are: `None`, `PastTense`. Defaults to `None` (no formatting required, i.e. as-is)`.
**`eventTransaction`** | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer whereever generating event publishing logic. Usage will force a rollback of any underlying data transaction (where the provider supports TransactionScope) on failure, such as an `EventPublish` error. This is by no means implying a Distributed Transaction (DTC) should be invoked; this is only intended for a single data source that supports a TransactionScope to guarantee reliable event publishing. Defaults to `false`. This essentially defaults the `Entity.EventTransaction` where not otherwise specified.
diff --git a/docs/Entity-Entity-Config-Xml.md b/docs/Entity-Entity-Config-Xml.md
index 1ea22ea58..930ba85a4 100644
--- a/docs/Entity-Entity-Config-Xml.md
+++ b/docs/Entity-Entity-Config-Xml.md
@@ -130,7 +130,7 @@ Property | Description
-|-
`ManagerConstructor` | The access modifier for the generated `Manager` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
**`ManagerCtorParams`** | The comma seperated list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `Manager` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`ManagerExtensions` | Indicates whether the `Manager` extensions logic should be generated.
+`ManagerExtensions` | Indicates whether the `Manager` extensions logic should be generated. This can be overridden using `Operation.ManagerExtensions`.
**`Validator`** | The name of the .NET `Type` that will perform the validation. Only used for defaulting the `Create` and `Update` operation types (`Operation.Type`) where not specified explicitly.
`IValidator` | The name of the .NET Interface that the `Validator` implements/inherits. Only used for defaulting the `Create` and `Update` operation types (`Operation.Type`) where not specified explicitly.
@@ -143,11 +143,12 @@ Property | Description
-|-
`DataSvcCaching` | Indicates whether request-based `IRequestCache` caching is to be performed at the `DataSvc` layer to improve performance (i.e. reduce chattiness). Defaults to `true`.
`EventPublish` | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+`EventSource` | The Event Source. Defaults to `Name` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
`EventSubjectFormat` | The default formatting for the Subject when an Event is published. Valid options are: `NameOnly`, `NameAndKey`. Defaults to `CodeGeneration.EventSubjectFormat`.
**`EventTransaction`** | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer whereever generating event publishing logic. Usage will force a rollback of any underlying data transaction (where the provider supports TransactionScope) on failure, such as an `EventPublish` error. This is by no means implying a Distributed Transaction (DTC) should be invoked; this is only intended for a single data source that supports a TransactionScope to guarantee reliable event publishing. Defaults to `CodeGeneration.EventTransaction`. This essentially defaults the `Operation.DataSvcTransaction` where not otherwise specified.
`DataSvcConstructor` | The access modifier for the generated `DataSvc` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
**`DataSvcCtorParams`** | The comma seperated list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `DataSvc` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`DataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated.
+`DataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated. This can be overridden using `Operation.DataSvcExtensions`.
@@ -160,7 +161,7 @@ Property | Description
`MapperAddStandardProperties` | Indicates that the `AddStandardProperties` method call is to be included for the generated (corresponding) `Mapper`. Defaults to `true`.
`DataConstructor` | The access modifier for the generated `Data` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
**`DataCtorParams`** | The comma seperated list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `Data` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`DataExtensions` | Indicates whether the `Data` extensions logic should be generated.
+`DataExtensions` | Indicates whether the `Data` extensions logic should be generated. This can be overridden using `Operation.DataExtensions`.
diff --git a/docs/Entity-Entity-Config.md b/docs/Entity-Entity-Config.md
index 60557e4f4..4c8ec2572 100644
--- a/docs/Entity-Entity-Config.md
+++ b/docs/Entity-Entity-Config.md
@@ -148,7 +148,7 @@ Property | Description
-|-
`managerCtor` | The access modifier for the generated `Manager` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
**`managerCtorParams`** | The list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `Manager` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`managerExtensions` | Indicates whether the `Manager` extensions logic should be generated.
+`managerExtensions` | Indicates whether the `Manager` extensions logic should be generated. This can be overridden using `Operation.ManagerExtensions`.
**`validator`** | The name of the .NET `Type` that will perform the validation. Only used for defaulting the `Create` and `Update` operation types (`Operation.Type`) where not specified explicitly.
`iValidator` | The name of the .NET Interface that the `Validator` implements/inherits. Only used for defaulting the `Create` and `Update` operation types (`Operation.Type`) where not specified explicitly.
@@ -161,11 +161,12 @@ Property | Description
-|-
`dataSvcCaching` | Indicates whether request-based `IRequestCache` caching is to be performed at the `DataSvc` layer to improve performance (i.e. reduce chattiness). Defaults to `true`.
`eventPublish` | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+`eventSource` | The Event Source. Defaults to `Name` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
`eventSubjectFormat` | The default formatting for the Subject when an Event is published. Valid options are: `NameOnly`, `NameAndKey`. Defaults to `CodeGeneration.EventSubjectFormat`.
**`eventTransaction`** | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer whereever generating event publishing logic. Usage will force a rollback of any underlying data transaction (where the provider supports TransactionScope) on failure, such as an `EventPublish` error. This is by no means implying a Distributed Transaction (DTC) should be invoked; this is only intended for a single data source that supports a TransactionScope to guarantee reliable event publishing. Defaults to `CodeGeneration.EventTransaction`. This essentially defaults the `Operation.DataSvcTransaction` where not otherwise specified.
`dataSvcCtor` | The access modifier for the generated `DataSvc` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
`dataSvcCtorParams` | The list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `DataSvc` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`dataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated.
+`dataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated. This can be overridden using `Operation.DataSvcExtensions`.
@@ -178,7 +179,7 @@ Property | Description
`mapperAddStandardProperties` | Indicates that the `AddStandardProperties` method call is to be included for the generated (corresponding) `Mapper`. Defaults to `true`.
`dataCtor` | The access modifier for the generated `Data` constructor. Valid options are: `Public`, `Private`, `Protected`. Defaults to `Public`.
`dataCtorParams` | The list of additional (non-inferred) Dependency Injection (DI) parameters for the generated `Data` constructor. Each constructor parameter should be formatted as `Type` + `^` + `Name`; e.g. `IConfiguration^Config`. Where the `Name` portion is not specified it will be inferred. Where the `Type` matches an already inferred value it will be ignored.
-`dataExtensions` | Indicates whether the `Data` extensions logic should be generated.
+`dataExtensions` | Indicates whether the `Data` extensions logic should be generated. This can be overridden using `Operation.DataExtensions`.
diff --git a/docs/Entity-Operation-Config-Xml.md b/docs/Entity-Operation-Config-Xml.md
index a678083be..c343c4554 100644
--- a/docs/Entity-Operation-Config-Xml.md
+++ b/docs/Entity-Operation-Config-Xml.md
@@ -95,7 +95,9 @@ Property | Description
-|-
**`DataSvcCustom`** | Indicates whether the `DataSvc` logic is a custom implementation; i.e. no auto-`DataSvc` invocation logic is to be generated.
`DataSvcTransaction` | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer.
+`DataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
`EventPublish` | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` or `Entity.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+`EventSource` | The Event Source. Defaults to `Entity.EventSource`. Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
`EventSubject` | The event subject template and corresponding event action pair (separated by a colon). The event subject template defaults to `{AppName}.{Entity.Name}`, plus each of the unique key placeholders comma separated; e.g. `Domain.Entity.{id1},{id2}` (depending on whether `Entity.EventSubjectFormat` is `NameAndKey` or `NameOnly`). The event action defaults to `WebApiOperationType` or `Operation.Type` where not specified. Multiple events can be raised by specifying more than one subject/action pair separated by a semicolon. E.g. `Demo.Person.{id}:Create;Demo.Other.{id}:Update`.
@@ -107,9 +109,11 @@ Property | Description
-|-
**`AutoImplement`** | The operation override for the `Entity.AutoImplement`. Valid options are: `Database`, `EntityFramework`, `Cosmos`, `OData`, `None`. Defaults to `Entity.AutoImplement`. The corresponding `Entity.AutoImplement` must be defined for this to be enacted. Auto-implementation is applicable for all `Operation.Type` options with the exception of `Custom`.
`DataEntityMapper` | The override for the data entity `Mapper`. Used where the default generated `Mapper` is not applicable.
+`DataExtensions` | Indicates whether the `Data` extensions logic should be generated. Defaults to `Entity.DataExtensions`.
`DatabaseStoredProc` | The database stored procedure name used where `Operation.AutoImplement` is `Database`. Defaults to `sp` + `Entity.Name` + `Operation.Name`; e.g. `spPersonCreate`.
`DataCosmosContainerId` | The Cosmos `ContainerId` override used where `Operation.AutoImplement` is `Cosmos`. Overrides the `Entity.CosmosContainerId`.
`DataCosmosPartitionKey` | The C# code override to be used for setting the optional Cosmos `PartitionKey` used where `Operation.AutoImplement` is `Cosmos`. Overrides the `Entity.CosmosPartitionKey`.
+`ManagerExtensions` | Indicates whether the `Manager` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
diff --git a/docs/Entity-Operation-Config.md b/docs/Entity-Operation-Config.md
index b6b22099f..a442c4746 100644
--- a/docs/Entity-Operation-Config.md
+++ b/docs/Entity-Operation-Config.md
@@ -114,7 +114,9 @@ Property | Description
-|-
**`dataSvcCustom`** | Indicates whether the `DataSvc` logic is a custom implementation; i.e. no auto-`DataSvc` invocation logic is to be generated.
`dataSvcTransaction` | Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer.
+`dataSvcExtensions` | Indicates whether the `DataSvc` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
`eventPublish` | Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` or `Entity.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+`eventSource` | The Event Source. Defaults to `Entity.EventSource`. Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
`eventSubject` | The event subject template and corresponding event action pair (separated by a colon). The event subject template defaults to `{AppName}.{Entity.Name}`, plus each of the unique key placeholders comma separated; e.g. `Domain.Entity.{id1},{id2}` (depending on whether `Entity.EventSubjectFormat` is `NameAndKey` or `NameOnly`). The event action defaults to `WebApiOperationType` or `Operation.Type` where not specified. Multiple events can be raised by specifying more than one subject/action pair separated by a semicolon. E.g. `Demo.Person.{id}:Create;Demo.Other.{id}:Update`.
@@ -126,9 +128,11 @@ Property | Description
-|-
**`autoImplement`** | The operation override for the `Entity.AutoImplement`. Valid options are: `Database`, `EntityFramework`, `Cosmos`, `OData`, `None`. Defaults to `Entity.AutoImplement`. The corresponding `Entity.AutoImplement` must be defined for this to be enacted. Auto-implementation is applicable for all `Operation.Type` options with the exception of `Custom`.
`dataEntityMapper` | The override for the data entity `Mapper`. Used where the default generated `Mapper` is not applicable.
+`dataExtensions` | Indicates whether the `Data` extensions logic should be generated. Defaults to `Entity.DataExtensions`.
`databaseStoredProc` | The database stored procedure name used where `Operation.AutoImplement` is `Database`. Defaults to `sp` + `Entity.Name` + `Operation.Name`; e.g. `spPersonCreate`.
`cosmosContainerId` | The Cosmos `ContainerId` override used where `Operation.AutoImplement` is `Cosmos`. Overrides the `Entity.CosmosContainerId`.
`cosmosPartitionKey` | The C# code override to be used for setting the optional Cosmos `PartitionKey` used where `Operation.AutoImplement` is `Cosmos`. Overrides the `Entity.CosmosPartitionKey`.
+`managerExtensions` | Indicates whether the `Manager` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs b/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs
index d782d52dd..c1c4b54f5 100644
--- a/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs
+++ b/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs
@@ -66,8 +66,7 @@ public Task GetAccountsAsync(AccountArgs? args, PagingA
return __val;
var __result = await _data.GetDetailAsync(accountId).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -85,8 +84,7 @@ public Task GetAccountsAsync(AccountArgs? args, PagingA
return __val;
var __result = await _data.GetBalanceAsync(accountId).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
}
diff --git a/samples/Cdr.Banking/Cdr.Banking.CodeGen/Properties/launchSettings.json b/samples/Cdr.Banking/Cdr.Banking.CodeGen/Properties/launchSettings.json
index f384979a4..8e0fc5e64 100644
--- a/samples/Cdr.Banking/Cdr.Banking.CodeGen/Properties/launchSettings.json
+++ b/samples/Cdr.Banking/Cdr.Banking.CodeGen/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"Cdr.Banking.CodeGen": {
"commandName": "Project",
- "commandLineArgs": "entity",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\samples\\Cdr.Banking\\Cdr.Banking.CodeGen"
+ "commandLineArgs": "all"
}
}
}
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.Api/Startup.cs b/samples/Demo/Beef.Demo.Api/Startup.cs
index ef674b492..ed3666222 100644
--- a/samples/Demo/Beef.Demo.Api/Startup.cs
+++ b/samples/Demo/Beef.Demo.Api/Startup.cs
@@ -1,4 +1,5 @@
-using Beef.AspNetCore.WebApi;
+using Azure.Messaging.EventHubs.Producer;
+using Beef.AspNetCore.WebApi;
using Beef.Caching.Policy;
using Beef.Data.Cosmos;
using Beef.Data.Database;
@@ -70,7 +71,7 @@ public void ConfigureServices(IServiceCollection services)
// Add event publishing.
var ehcs = _config.GetValue("EventHubConnectionString");
if (!string.IsNullOrEmpty(ehcs))
- services.AddBeefEventHubEventProducer(ehcs);
+ services.AddBeefEventHubEventProducer(new EventHubProducerClient(ehcs));
else
services.AddBeefNullEventPublisher();
diff --git a/samples/Demo/Beef.Demo.Business/Data/Generated/PersonData.cs b/samples/Demo/Beef.Demo.Business/Data/Generated/PersonData.cs
index ebcde77d9..da784ae9f 100644
--- a/samples/Demo/Beef.Demo.Business/Data/Generated/PersonData.cs
+++ b/samples/Demo/Beef.Demo.Business/Data/Generated/PersonData.cs
@@ -40,12 +40,6 @@ public partial class PersonData : IPersonData
private Func? _deleteOnBeforeAsync;
private Func? _deleteOnAfterAsync;
private Action? _deleteOnException;
- private Func? _getOnBeforeAsync;
- private Func? _getOnAfterAsync;
- private Action? _getOnException;
- private Func? _updateOnBeforeAsync;
- private Func? _updateOnAfterAsync;
- private Action? _updateOnException;
private Func? _updateWithRollbackOnBeforeAsync;
private Func? _updateWithRollbackOnAfterAsync;
private Action? _updateWithRollbackOnException;
@@ -119,9 +113,9 @@ public Task CreateAsync(Person value)
{
Person __result;
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonCreate]");
- if (_createOnBeforeAsync != null) await _createOnBeforeAsync(value, __dataArgs).ConfigureAwait(false);
+ await (_createOnBeforeAsync?.Invoke(value, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result = await _db.CreateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_createOnAfterAsync != null) await _createOnAfterAsync(__result).ConfigureAwait(false);
+ await (_createOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _createOnException });
}
@@ -135,9 +129,9 @@ public Task DeleteAsync(Guid id)
return DataInvoker.Current.InvokeAsync(this, async () =>
{
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonDelete]");
- if (_deleteOnBeforeAsync != null) await _deleteOnBeforeAsync(id, __dataArgs).ConfigureAwait(false);
+ await (_deleteOnBeforeAsync?.Invoke(id, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
await _db.DeleteAsync(__dataArgs, id).ConfigureAwait(false);
- if (_deleteOnAfterAsync != null) await _deleteOnAfterAsync(id).ConfigureAwait(false);
+ await (_deleteOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
}, new BusinessInvokerArgs { ExceptionHandler = _deleteOnException });
}
@@ -150,13 +144,9 @@ public Task DeleteAsync(Guid id)
{
return DataInvoker.Current.InvokeAsync(this, async () =>
{
- Person? __result;
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonGet]");
- if (_getOnBeforeAsync != null) await _getOnBeforeAsync(id, __dataArgs).ConfigureAwait(false);
- __result = await _db.GetAsync(__dataArgs, id).ConfigureAwait(false);
- if (_getOnAfterAsync != null) await _getOnAfterAsync(__result, id).ConfigureAwait(false);
- return __result;
- }, new BusinessInvokerArgs { ExceptionHandler = _getOnException });
+ return await _db.GetAsync(__dataArgs, id).ConfigureAwait(false);
+ });
}
///
@@ -168,13 +158,9 @@ public Task UpdateAsync(Person value)
{
return DataInvoker.Current.InvokeAsync(this, async () =>
{
- Person __result;
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonUpdate]");
- if (_updateOnBeforeAsync != null) await _updateOnBeforeAsync(value, __dataArgs).ConfigureAwait(false);
- __result = await _db.UpdateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateOnAfterAsync != null) await _updateOnAfterAsync(__result).ConfigureAwait(false);
- return __result;
- }, new BusinessInvokerArgs { ExceptionHandler = _updateOnException });
+ return await _db.UpdateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
+ });
}
///
@@ -188,9 +174,9 @@ public Task UpdateWithRollbackAsync(Person value)
{
Person __result;
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonUpdate]");
- if (_updateWithRollbackOnBeforeAsync != null) await _updateWithRollbackOnBeforeAsync(value, __dataArgs).ConfigureAwait(false);
+ await (_updateWithRollbackOnBeforeAsync?.Invoke(value, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result = await _db.UpdateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateWithRollbackOnAfterAsync != null) await _updateWithRollbackOnAfterAsync(__result).ConfigureAwait(false);
+ await (_updateWithRollbackOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _updateWithRollbackOnException });
}
@@ -206,9 +192,9 @@ public Task GetAllAsync(PagingArgs? paging)
{
PersonCollectionResult __result = new PersonCollectionResult(paging);
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonGetAll]", __result.Paging!);
- if (_getAllOnBeforeAsync != null) await _getAllOnBeforeAsync(__dataArgs).ConfigureAwait(false);
+ await (_getAllOnBeforeAsync?.Invoke(__dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result.Result = await _db.Query(__dataArgs, p => _getAllOnQuery?.Invoke(p, __dataArgs)).SelectQueryAsync().ConfigureAwait(false);
- if (_getAllOnAfterAsync != null) await _getAllOnAfterAsync(__result).ConfigureAwait(false);
+ await (_getAllOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _getAllOnException });
}
@@ -223,9 +209,9 @@ public Task GetAll2Async()
{
PersonCollectionResult __result = new PersonCollectionResult();
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonGetAll]");
- if (_getAll2OnBeforeAsync != null) await _getAll2OnBeforeAsync(__dataArgs).ConfigureAwait(false);
+ await (_getAll2OnBeforeAsync?.Invoke(__dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result.Result = await _db.Query(__dataArgs, p => _getAll2OnQuery?.Invoke(p, __dataArgs)).SelectQueryAsync().ConfigureAwait(false);
- if (_getAll2OnAfterAsync != null) await _getAll2OnAfterAsync(__result).ConfigureAwait(false);
+ await (_getAll2OnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _getAll2OnException });
}
@@ -242,9 +228,9 @@ public Task GetByArgsAsync(PersonArgs? args, PagingArgs?
{
PersonCollectionResult __result = new PersonCollectionResult(paging);
var __dataArgs = DbMapper.Default.CreateArgs("[Demo].[spPersonGetByArgs]", __result.Paging!);
- if (_getByArgsOnBeforeAsync != null) await _getByArgsOnBeforeAsync(args, __dataArgs).ConfigureAwait(false);
+ await (_getByArgsOnBeforeAsync?.Invoke(args, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result.Result = await _db.Query(__dataArgs, p => _getByArgsOnQuery?.Invoke(p, args, __dataArgs)).SelectQueryAsync().ConfigureAwait(false);
- if (_getByArgsOnAfterAsync != null) await _getByArgsOnAfterAsync(__result, args).ConfigureAwait(false);
+ await (_getByArgsOnAfterAsync?.Invoke(__result, args) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _getByArgsOnException });
}
@@ -325,9 +311,9 @@ public Task GetByArgsWithEfAsync(PersonArgs? args, Pagin
{
PersonCollectionResult __result = new PersonCollectionResult(paging);
var __dataArgs = EfMapper.Default.CreateArgs(__result.Paging!);
- if (_getByArgsWithEfOnBeforeAsync != null) await _getByArgsWithEfOnBeforeAsync(args, __dataArgs).ConfigureAwait(false);
+ await (_getByArgsWithEfOnBeforeAsync?.Invoke(args, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result.Result = _ef.Query(__dataArgs, q => _getByArgsWithEfOnQuery?.Invoke(q, args, __dataArgs) ?? q).SelectQuery();
- if (_getByArgsWithEfOnAfterAsync != null) await _getByArgsWithEfOnAfterAsync(__result, args).ConfigureAwait(false);
+ await (_getByArgsWithEfOnAfterAsync?.Invoke(__result, args) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _getByArgsWithEfOnException });
}
@@ -357,9 +343,9 @@ public Task ThrowErrorAsync()
{
Person? __result;
var __dataArgs = EfMapper.Default.CreateArgs();
- if (_getWithEfOnBeforeAsync != null) await _getWithEfOnBeforeAsync(id, __dataArgs).ConfigureAwait(false);
+ await (_getWithEfOnBeforeAsync?.Invoke(id, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result = await _ef.GetAsync(__dataArgs, id).ConfigureAwait(false);
- if (_getWithEfOnAfterAsync != null) await _getWithEfOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_getWithEfOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _getWithEfOnException });
}
@@ -375,9 +361,9 @@ public Task CreateWithEfAsync(Person value)
{
Person __result;
var __dataArgs = EfMapper.Default.CreateArgs();
- if (_createWithEfOnBeforeAsync != null) await _createWithEfOnBeforeAsync(value, __dataArgs).ConfigureAwait(false);
+ await (_createWithEfOnBeforeAsync?.Invoke(value, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result = await _ef.CreateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_createWithEfOnAfterAsync != null) await _createWithEfOnAfterAsync(__result).ConfigureAwait(false);
+ await (_createWithEfOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _createWithEfOnException });
}
@@ -393,9 +379,9 @@ public Task UpdateWithEfAsync(Person value)
{
Person __result;
var __dataArgs = EfMapper.Default.CreateArgs();
- if (_updateWithEfOnBeforeAsync != null) await _updateWithEfOnBeforeAsync(value, __dataArgs).ConfigureAwait(false);
+ await (_updateWithEfOnBeforeAsync?.Invoke(value, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
__result = await _ef.UpdateAsync(__dataArgs, Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateWithEfOnAfterAsync != null) await _updateWithEfOnAfterAsync(__result).ConfigureAwait(false);
+ await (_updateWithEfOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { ExceptionHandler = _updateWithEfOnException });
}
@@ -409,9 +395,9 @@ public Task DeleteWithEfAsync(Guid id)
return DataInvoker.Current.InvokeAsync(this, async () =>
{
var __dataArgs = EfMapper.Default.CreateArgs();
- if (_deleteWithEfOnBeforeAsync != null) await _deleteWithEfOnBeforeAsync(id, __dataArgs).ConfigureAwait(false);
+ await (_deleteWithEfOnBeforeAsync?.Invoke(id, __dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
await _ef.DeleteAsync(__dataArgs, id).ConfigureAwait(false);
- if (_deleteWithEfOnAfterAsync != null) await _deleteWithEfOnAfterAsync(id).ConfigureAwait(false);
+ await (_deleteWithEfOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
}, new BusinessInvokerArgs { ExceptionHandler = _deleteWithEfOnException });
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs
index 77995123d..8bb325f99 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs
@@ -67,8 +67,7 @@ public Task GetAllAsync()
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -82,9 +81,8 @@ public Task CreateAsync(Contact value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Contact.{__result.Id}", "Create").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/contact", UriKind.Relative), $"Demo.Contact.{_evtPub.FormatKey(__result)}", "Create").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -98,9 +96,8 @@ public Task UpdateAsync(Contact value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Contact.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/contact", UriKind.Relative), $"Demo.Contact.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -113,7 +110,7 @@ public Task DeleteAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"Demo.Contact.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
+ await _evtPub.PublishValue(new Contact { Id = id }, new Uri($"/contact", UriKind.Relative), $"Demo.Contact.{_evtPub.FormatKey(id)}", "Delete", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
});
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/GenderDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/GenderDataSvc.cs
index efe9d9747..a6c1bec90 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/GenderDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/GenderDataSvc.cs
@@ -54,8 +54,7 @@ public GenderDataSvc(IGenderData data, IEventPublisher evtPub, IRequestCache cac
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -69,9 +68,8 @@ public Task CreateAsync(Gender value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Gender.{__result.Id}", "Create").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"Demo.Gender.{_evtPub.FormatKey(__result)}", "Create").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -85,9 +83,8 @@ public Task UpdateAsync(Gender value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Gender.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"Demo.Gender.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/PersonDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/PersonDataSvc.cs
index 797b19c59..4cebf1521 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/PersonDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/PersonDataSvc.cs
@@ -33,8 +33,6 @@ public partial class PersonDataSvc : IPersonDataSvc
private Func? _createOnAfterAsync;
private Func? _deleteOnAfterAsync;
- private Func? _getOnAfterAsync;
- private Func? _updateOnAfterAsync;
private Func? _updateWithRollbackOnAfterAsync;
private Func? _getAllOnAfterAsync;
private Func? _getAll2OnAfterAsync;
@@ -78,10 +76,9 @@ public Task CreateAsync(Person value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_createOnAfterAsync != null) await _createOnAfterAsync(__result).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Person.{__result.Id}", "Create").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await (_createOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(__result)}", "Create").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -94,8 +91,8 @@ public Task DeleteAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- if (_deleteOnAfterAsync != null) await _deleteOnAfterAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"Demo.Person.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
+ await (_deleteOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(new Person { Id = id }, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(id)}", "Delete", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -114,9 +111,7 @@ public Task DeleteAsync(Guid id)
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- if (_getOnAfterAsync != null) await _getOnAfterAsync(__result, id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -130,10 +125,8 @@ public Task UpdateAsync(Person value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateOnAfterAsync != null) await _updateOnAfterAsync(__result).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Person.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -147,10 +140,9 @@ public Task UpdateWithRollbackAsync(Person value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateWithRollbackAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateWithRollbackOnAfterAsync != null) await _updateWithRollbackOnAfterAsync(__result).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Person.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await (_updateWithRollbackOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -164,7 +156,7 @@ public Task GetAllAsync(PagingArgs? paging)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetAllAsync(paging).ConfigureAwait(false);
- if (_getAllOnAfterAsync != null) await _getAllOnAfterAsync(__result, paging).ConfigureAwait(false);
+ await (_getAllOnAfterAsync?.Invoke(__result, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -178,7 +170,7 @@ public Task GetAll2Async()
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetAll2Async().ConfigureAwait(false);
- if (_getAll2OnAfterAsync != null) await _getAll2OnAfterAsync(__result).ConfigureAwait(false);
+ await (_getAll2OnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -194,7 +186,7 @@ public Task GetByArgsAsync(PersonArgs? args, PagingArgs?
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetByArgsAsync(args, paging).ConfigureAwait(false);
- if (_getByArgsOnAfterAsync != null) await _getByArgsOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getByArgsOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -210,7 +202,7 @@ public Task GetDetailByArgsAsync(PersonArgs? args,
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetDetailByArgsAsync(args, paging).ConfigureAwait(false);
- if (_getDetailByArgsOnAfterAsync != null) await _getDetailByArgsOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getDetailByArgsOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -226,10 +218,10 @@ public Task MergeAsync(Guid fromId, Guid toId)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.MergeAsync(fromId, toId).ConfigureAwait(false);
- if (_mergeOnAfterAsync != null) await _mergeOnAfterAsync(__result, fromId, toId).ConfigureAwait(false);
+ await (_mergeOnAfterAsync?.Invoke(__result, fromId, toId) ?? Task.CompletedTask).ConfigureAwait(false);
await _evtPub.Publish(
- _evtPub.CreateValueEvent(__result, $"Demo.Person.{fromId}", "MergeFrom", fromId, toId),
- _evtPub.CreateValueEvent(__result, $"Demo.Person.{toId}", "MergeTo", fromId, toId)).SendAsync().ConfigureAwait(false);
+ _evtPub.CreateValueEvent(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{fromId}", "MergeFrom", fromId, toId),
+ _evtPub.CreateValueEvent(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{toId}", "MergeTo", fromId, toId)).SendAsync().ConfigureAwait(false);
return __result;
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
@@ -243,7 +235,7 @@ public Task MarkAsync()
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.MarkAsync().ConfigureAwait(false);
- if (_markOnAfterAsync != null) await _markOnAfterAsync().ConfigureAwait(false);
+ await (_markOnAfterAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
await _evtPub.SendAsync().ConfigureAwait(false);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -258,7 +250,7 @@ public Task MapAsync(MapArgs? args)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.MapAsync(args).ConfigureAwait(false);
- if (_mapOnAfterAsync != null) await _mapOnAfterAsync(__result, args).ConfigureAwait(false);
+ await (_mapOnAfterAsync?.Invoke(__result, args) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -276,9 +268,8 @@ public Task MapAsync(MapArgs? args)
return __val;
var __result = await _data.GetNoArgsAsync().ConfigureAwait(false);
- if (_getNoArgsOnAfterAsync != null) await _getNoArgsOnAfterAsync(__result).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ await (_getNoArgsOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -296,9 +287,8 @@ public Task MapAsync(MapArgs? args)
return __val;
var __result = await _data.GetDetailAsync(id).ConfigureAwait(false);
- if (_getDetailOnAfterAsync != null) await _getDetailOnAfterAsync(__result, id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ await (_getDetailOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -312,10 +302,9 @@ public Task UpdateDetailAsync(PersonDetail value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateDetailAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateDetailOnAfterAsync != null) await _updateDetailOnAfterAsync(__result).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Person.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await (_updateDetailOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -337,7 +326,7 @@ public Task DataSvcCustomAsync()
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetNullAsync(name, names).ConfigureAwait(false);
- if (_getNullOnAfterAsync != null) await _getNullOnAfterAsync(__result, name, names).ConfigureAwait(false);
+ await (_getNullOnAfterAsync?.Invoke(__result, name, names) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -361,7 +350,7 @@ public Task GetByArgsWithEfAsync(PersonArgs? args, Pagin
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.GetByArgsWithEfAsync(args, paging).ConfigureAwait(false);
- if (_getByArgsWithEfOnAfterAsync != null) await _getByArgsWithEfOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getByArgsWithEfOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -374,7 +363,7 @@ public Task ThrowErrorAsync()
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.ThrowErrorAsync().ConfigureAwait(false);
- if (_throwErrorOnAfterAsync != null) await _throwErrorOnAfterAsync().ConfigureAwait(false);
+ await (_throwErrorOnAfterAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
});
}
@@ -388,7 +377,7 @@ public Task ThrowErrorAsync()
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.InvokeApiViaAgentAsync(id).ConfigureAwait(false);
- if (_invokeApiViaAgentOnAfterAsync != null) await _invokeApiViaAgentOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_invokeApiViaAgentOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return __result;
});
}
@@ -407,9 +396,8 @@ public Task ThrowErrorAsync()
return __val;
var __result = await _data.GetWithEfAsync(id).ConfigureAwait(false);
- if (_getWithEfOnAfterAsync != null) await _getWithEfOnAfterAsync(__result, id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ await (_getWithEfOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -423,9 +411,8 @@ public Task CreateWithEfAsync(Person value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateWithEfAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_createWithEfOnAfterAsync != null) await _createWithEfOnAfterAsync(__result).ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await (_createWithEfOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -439,10 +426,9 @@ public Task UpdateWithEfAsync(Person value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateWithEfAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- if (_updateWithEfOnAfterAsync != null) await _updateWithEfOnAfterAsync(__result).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Person.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await (_updateWithEfOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(__result, new Uri($"/person", UriKind.Relative), $"Demo.Person.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
@@ -455,8 +441,8 @@ public Task DeleteWithEfAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteWithEfAsync(id).ConfigureAwait(false);
- if (_deleteWithEfOnAfterAsync != null) await _deleteWithEfOnAfterAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"Demo.Person.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
+ await (_deleteWithEfOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
+ await _evtPub.PublishValue(new Person { Id = id }, new Uri($"/person", UriKind.Relative), $"Demo.Person.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
}, new BusinessInvokerArgs { IncludeTransactionScope = true });
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/RobotDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/RobotDataSvc.cs
index 74ac61584..9776cdf7f 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/RobotDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/RobotDataSvc.cs
@@ -54,8 +54,7 @@ public RobotDataSvc(IRobotData data, IEventPublisher evtPub, IRequestCache cache
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -69,9 +68,8 @@ public Task CreateAsync(Robot value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Robot.{__result.Id}", "Create").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/robots/{_evtPub.FormatKey(__result)}", UriKind.Relative), $"Demo.Robot.{_evtPub.FormatKey(__result)}", "Create").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -85,9 +83,8 @@ public Task UpdateAsync(Robot value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.Robot.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/robots/{_evtPub.FormatKey(__result)}", UriKind.Relative), $"Demo.Robot.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -100,7 +97,7 @@ public Task DeleteAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"Demo.Robot.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
+ await _evtPub.PublishValue(new Robot { Id = id }, new Uri($"/robots/{_evtPub.FormatKey(id)}", UriKind.Relative), $"Demo.Robot.{_evtPub.FormatKey(id)}", "Delete", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
});
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/TripPersonDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/TripPersonDataSvc.cs
index c4e1d73aa..62134831f 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/TripPersonDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/TripPersonDataSvc.cs
@@ -54,8 +54,7 @@ public TripPersonDataSvc(ITripPersonData data, IEventPublisher evtPub, IRequestC
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -69,9 +68,8 @@ public Task CreateAsync(TripPerson value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.TripPerson.{__result.Id}", "Create").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/tripperson", UriKind.Relative), $"Demo.TripPerson.{_evtPub.FormatKey(__result)}", "Create").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -85,9 +83,8 @@ public Task UpdateAsync(TripPerson value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"Demo.TripPerson.{__result.Id}", "Update").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, new Uri($"/tripperson", UriKind.Relative), $"Demo.TripPerson.{_evtPub.FormatKey(__result)}", "Update").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -100,7 +97,7 @@ public Task DeleteAsync(string? id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"Demo.TripPerson.{id}", "Delete", id).SendAsync().ConfigureAwait(false);
+ await _evtPub.PublishValue(new TripPerson { Id = id }, new Uri($"/tripperson", UriKind.Relative), $"Demo.TripPerson.{_evtPub.FormatKey(id)}", "Delete", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
});
}
diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/PersonDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/PersonDataSvc.cs
index 4611de56a..12e5d13e5 100644
--- a/samples/Demo/Beef.Demo.Business/DataSvc/PersonDataSvc.cs
+++ b/samples/Demo/Beef.Demo.Business/DataSvc/PersonDataSvc.cs
@@ -1,4 +1,5 @@
using Beef.Demo.Common.Entities;
+using Beef.Events;
using System;
using System.Threading.Tasks;
diff --git a/samples/Demo/Beef.Demo.Business/Generated/PersonManager.cs b/samples/Demo/Beef.Demo.Business/Generated/PersonManager.cs
index c117439e1..614b6a8c1 100644
--- a/samples/Demo/Beef.Demo.Business/Generated/PersonManager.cs
+++ b/samples/Demo/Beef.Demo.Business/Generated/PersonManager.cs
@@ -40,11 +40,6 @@ public partial class PersonManager : IPersonManager
private Func? _deleteOnBeforeAsync;
private Func? _deleteOnAfterAsync;
- private Func? _getOnPreValidateAsync;
- private Action? _getOnValidate;
- private Func? _getOnBeforeAsync;
- private Func? _getOnAfterAsync;
-
private Func? _updateOnPreValidateAsync;
private Action? _updateOnValidate;
private Func? _updateOnBeforeAsync;
@@ -180,16 +175,16 @@ public async Task CreateAsync(Person value)
{
value.Id = await _guidIdGen.GenerateIdentifierAsync().ConfigureAwait(false);
Cleaner.CleanUp(value);
- if (_createOnPreValidateAsync != null) await _createOnPreValidateAsync(value).ConfigureAwait(false);
+ await (_createOnPreValidateAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _createOnValidate?.Invoke(__mv, value))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_createOnBeforeAsync != null) await _createOnBeforeAsync(value).ConfigureAwait(false);
+ await (_createOnBeforeAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.CreateAsync(value).ConfigureAwait(false);
- if (_createOnAfterAsync != null) await _createOnAfterAsync(__result).ConfigureAwait(false);
+ await (_createOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Create).ConfigureAwait(false);
}
@@ -203,16 +198,16 @@ public async Task DeleteAsync(Guid id)
await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_deleteOnPreValidateAsync != null) await _deleteOnPreValidateAsync(id).ConfigureAwait(false);
+ await (_deleteOnPreValidateAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(id.Validate(nameof(id)).Mandatory())
.Additional((__mv) => _deleteOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_deleteOnBeforeAsync != null) await _deleteOnBeforeAsync(id).ConfigureAwait(false);
+ await (_deleteOnBeforeAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
await _dataService.DeleteAsync(id).ConfigureAwait(false);
- if (_deleteOnAfterAsync != null) await _deleteOnAfterAsync(id).ConfigureAwait(false);
+ await (_deleteOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
}, BusinessInvokerArgs.Delete).ConfigureAwait(false);
}
@@ -226,17 +221,11 @@ await ManagerInvoker.Current.InvokeAsync(this, async () =>
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_getOnPreValidateAsync != null) await _getOnPreValidateAsync(id).ConfigureAwait(false);
-
(await MultiValidator.Create()
.Add(id.Validate(nameof(id)).Mandatory())
- .Additional((__mv) => _getOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getOnBeforeAsync != null) await _getOnBeforeAsync(id).ConfigureAwait(false);
- var __result = await _dataService.GetAsync(id).ConfigureAwait(false);
- if (_getOnAfterAsync != null) await _getOnAfterAsync(__result, id).ConfigureAwait(false);
- return Cleaner.Clean(__result);
+ return Cleaner.Clean(await _dataService.GetAsync(id).ConfigureAwait(false));
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -254,16 +243,16 @@ public async Task UpdateAsync(Person value, Guid id)
{
value.Id = id;
Cleaner.CleanUp(value);
- if (_updateOnPreValidateAsync != null) await _updateOnPreValidateAsync(value, id).ConfigureAwait(false);
+ await (_updateOnPreValidateAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _updateOnValidate?.Invoke(__mv, value, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_updateOnBeforeAsync != null) await _updateOnBeforeAsync(value, id).ConfigureAwait(false);
+ await (_updateOnBeforeAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.UpdateAsync(value).ConfigureAwait(false);
- if (_updateOnAfterAsync != null) await _updateOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_updateOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -282,16 +271,16 @@ public async Task UpdateWithRollbackAsync(Person value, Guid id)
{
value.Id = id;
Cleaner.CleanUp(value);
- if (_updateWithRollbackOnPreValidateAsync != null) await _updateWithRollbackOnPreValidateAsync(value, id).ConfigureAwait(false);
+ await (_updateWithRollbackOnPreValidateAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _updateWithRollbackOnValidate?.Invoke(__mv, value, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_updateWithRollbackOnBeforeAsync != null) await _updateWithRollbackOnBeforeAsync(value, id).ConfigureAwait(false);
+ await (_updateWithRollbackOnBeforeAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.UpdateWithRollbackAsync(value).ConfigureAwait(false);
- if (_updateWithRollbackOnAfterAsync != null) await _updateWithRollbackOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_updateWithRollbackOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -305,15 +294,15 @@ public async Task GetAllAsync(PagingArgs? paging)
{
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_getAllOnPreValidateAsync != null) await _getAllOnPreValidateAsync(paging).ConfigureAwait(false);
+ await (_getAllOnPreValidateAsync?.Invoke(paging) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _getAllOnValidate?.Invoke(__mv, paging))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getAllOnBeforeAsync != null) await _getAllOnBeforeAsync(paging).ConfigureAwait(false);
+ await (_getAllOnBeforeAsync?.Invoke(paging) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetAllAsync(paging).ConfigureAwait(false);
- if (_getAllOnAfterAsync != null) await _getAllOnAfterAsync(__result, paging).ConfigureAwait(false);
+ await (_getAllOnAfterAsync?.Invoke(__result, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -326,15 +315,15 @@ public async Task GetAll2Async()
{
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_getAll2OnPreValidateAsync != null) await _getAll2OnPreValidateAsync().ConfigureAwait(false);
+ await (_getAll2OnPreValidateAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _getAll2OnValidate?.Invoke(__mv))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getAll2OnBeforeAsync != null) await _getAll2OnBeforeAsync().ConfigureAwait(false);
+ await (_getAll2OnBeforeAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetAll2Async().ConfigureAwait(false);
- if (_getAll2OnAfterAsync != null) await _getAll2OnAfterAsync(__result).ConfigureAwait(false);
+ await (_getAll2OnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -350,16 +339,16 @@ public async Task GetByArgsAsync(PersonArgs? args, Pagin
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(args);
- if (_getByArgsOnPreValidateAsync != null) await _getByArgsOnPreValidateAsync(args, paging).ConfigureAwait(false);
+ await (_getByArgsOnPreValidateAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(args.Validate(nameof(args)).Entity().With>())
.Additional((__mv) => _getByArgsOnValidate?.Invoke(__mv, args, paging))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getByArgsOnBeforeAsync != null) await _getByArgsOnBeforeAsync(args, paging).ConfigureAwait(false);
+ await (_getByArgsOnBeforeAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetByArgsAsync(args, paging).ConfigureAwait(false);
- if (_getByArgsOnAfterAsync != null) await _getByArgsOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getByArgsOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -375,16 +364,16 @@ public async Task GetDetailByArgsAsync(PersonArgs?
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(args);
- if (_getDetailByArgsOnPreValidateAsync != null) await _getDetailByArgsOnPreValidateAsync(args, paging).ConfigureAwait(false);
+ await (_getDetailByArgsOnPreValidateAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(args.Validate(nameof(args)).Entity().With>())
.Additional((__mv) => _getDetailByArgsOnValidate?.Invoke(__mv, args, paging))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getDetailByArgsOnBeforeAsync != null) await _getDetailByArgsOnBeforeAsync(args, paging).ConfigureAwait(false);
+ await (_getDetailByArgsOnBeforeAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetDetailByArgsAsync(args, paging).ConfigureAwait(false);
- if (_getDetailByArgsOnAfterAsync != null) await _getDetailByArgsOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getDetailByArgsOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -400,7 +389,7 @@ public async Task MergeAsync(Guid fromId, Guid toId)
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(fromId, toId);
- if (_mergeOnPreValidateAsync != null) await _mergeOnPreValidateAsync(fromId, toId).ConfigureAwait(false);
+ await (_mergeOnPreValidateAsync?.Invoke(fromId, toId) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(fromId.Validate(nameof(fromId)).Mandatory())
@@ -408,9 +397,9 @@ public async Task MergeAsync(Guid fromId, Guid toId)
.Additional((__mv) => _mergeOnValidate?.Invoke(__mv, fromId, toId))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_mergeOnBeforeAsync != null) await _mergeOnBeforeAsync(fromId, toId).ConfigureAwait(false);
+ await (_mergeOnBeforeAsync?.Invoke(fromId, toId) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.MergeAsync(fromId, toId).ConfigureAwait(false);
- if (_mergeOnAfterAsync != null) await _mergeOnAfterAsync(__result, fromId, toId).ConfigureAwait(false);
+ await (_mergeOnAfterAsync?.Invoke(__result, fromId, toId) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -422,15 +411,15 @@ public async Task MarkAsync()
{
await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_markOnPreValidateAsync != null) await _markOnPreValidateAsync().ConfigureAwait(false);
+ await (_markOnPreValidateAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _markOnValidate?.Invoke(__mv))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_markOnBeforeAsync != null) await _markOnBeforeAsync().ConfigureAwait(false);
+ await (_markOnBeforeAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
await _dataService.MarkAsync().ConfigureAwait(false);
- if (_markOnAfterAsync != null) await _markOnAfterAsync().ConfigureAwait(false);
+ await (_markOnAfterAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -444,15 +433,15 @@ public async Task MapAsync(MapArgs? args)
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(args);
- if (_mapOnPreValidateAsync != null) await _mapOnPreValidateAsync(args).ConfigureAwait(false);
+ await (_mapOnPreValidateAsync?.Invoke(args) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _mapOnValidate?.Invoke(__mv, args))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_mapOnBeforeAsync != null) await _mapOnBeforeAsync(args).ConfigureAwait(false);
+ await (_mapOnBeforeAsync?.Invoke(args) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.MapAsync(args).ConfigureAwait(false);
- if (_mapOnAfterAsync != null) await _mapOnAfterAsync(__result, args).ConfigureAwait(false);
+ await (_mapOnAfterAsync?.Invoke(__result, args) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -465,15 +454,15 @@ public async Task MapAsync(MapArgs? args)
{
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_getNoArgsOnPreValidateAsync != null) await _getNoArgsOnPreValidateAsync().ConfigureAwait(false);
+ await (_getNoArgsOnPreValidateAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _getNoArgsOnValidate?.Invoke(__mv))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getNoArgsOnBeforeAsync != null) await _getNoArgsOnBeforeAsync().ConfigureAwait(false);
+ await (_getNoArgsOnBeforeAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetNoArgsAsync().ConfigureAwait(false);
- if (_getNoArgsOnAfterAsync != null) await _getNoArgsOnAfterAsync(__result).ConfigureAwait(false);
+ await (_getNoArgsOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -488,16 +477,16 @@ public async Task MapAsync(MapArgs? args)
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_getDetailOnPreValidateAsync != null) await _getDetailOnPreValidateAsync(id).ConfigureAwait(false);
+ await (_getDetailOnPreValidateAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(id.Validate(nameof(id)).Mandatory())
.Additional((__mv) => _getDetailOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getDetailOnBeforeAsync != null) await _getDetailOnBeforeAsync(id).ConfigureAwait(false);
+ await (_getDetailOnBeforeAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetDetailAsync(id).ConfigureAwait(false);
- if (_getDetailOnAfterAsync != null) await _getDetailOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_getDetailOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -516,16 +505,16 @@ public async Task UpdateDetailAsync(PersonDetail value, Guid id)
{
value.Id = id;
Cleaner.CleanUp(value);
- if (_updateDetailOnPreValidateAsync != null) await _updateDetailOnPreValidateAsync(value, id).ConfigureAwait(false);
+ await (_updateDetailOnPreValidateAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _updateDetailOnValidate?.Invoke(__mv, value, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_updateDetailOnBeforeAsync != null) await _updateDetailOnBeforeAsync(value, id).ConfigureAwait(false);
+ await (_updateDetailOnBeforeAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.UpdateDetailAsync(value).ConfigureAwait(false);
- if (_updateDetailOnAfterAsync != null) await _updateDetailOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_updateDetailOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -550,15 +539,15 @@ public async Task DataSvcCustomAsync()
{
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_dataSvcCustomOnPreValidateAsync != null) await _dataSvcCustomOnPreValidateAsync().ConfigureAwait(false);
+ await (_dataSvcCustomOnPreValidateAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _dataSvcCustomOnValidate?.Invoke(__mv))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_dataSvcCustomOnBeforeAsync != null) await _dataSvcCustomOnBeforeAsync().ConfigureAwait(false);
+ await (_dataSvcCustomOnBeforeAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.DataSvcCustomAsync().ConfigureAwait(false);
- if (_dataSvcCustomOnAfterAsync != null) await _dataSvcCustomOnAfterAsync(__result).ConfigureAwait(false);
+ await (_dataSvcCustomOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Unspecified).ConfigureAwait(false);
}
@@ -586,15 +575,15 @@ public async Task DataSvcCustomAsync()
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(name, names);
- if (_getNullOnPreValidateAsync != null) await _getNullOnPreValidateAsync(name, names).ConfigureAwait(false);
+ await (_getNullOnPreValidateAsync?.Invoke(name, names) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _getNullOnValidate?.Invoke(__mv, name, names))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getNullOnBeforeAsync != null) await _getNullOnBeforeAsync(name, names).ConfigureAwait(false);
+ await (_getNullOnBeforeAsync?.Invoke(name, names) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetNullAsync(name, names).ConfigureAwait(false);
- if (_getNullOnAfterAsync != null) await _getNullOnAfterAsync(__result, name, names).ConfigureAwait(false);
+ await (_getNullOnAfterAsync?.Invoke(__result, name, names) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Unspecified).ConfigureAwait(false);
}
@@ -611,16 +600,16 @@ public async Task EventPublishNoSendAsync(Person value)
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(value);
- if (_eventPublishNoSendOnPreValidateAsync != null) await _eventPublishNoSendOnPreValidateAsync(value).ConfigureAwait(false);
+ await (_eventPublishNoSendOnPreValidateAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _eventPublishNoSendOnValidate?.Invoke(__mv, value))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_eventPublishNoSendOnBeforeAsync != null) await _eventPublishNoSendOnBeforeAsync(value).ConfigureAwait(false);
+ await (_eventPublishNoSendOnBeforeAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.EventPublishNoSendAsync(value).ConfigureAwait(false);
- if (_eventPublishNoSendOnAfterAsync != null) await _eventPublishNoSendOnAfterAsync(__result).ConfigureAwait(false);
+ await (_eventPublishNoSendOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -636,16 +625,16 @@ public async Task GetByArgsWithEfAsync(PersonArgs? args,
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(args);
- if (_getByArgsWithEfOnPreValidateAsync != null) await _getByArgsWithEfOnPreValidateAsync(args, paging).ConfigureAwait(false);
+ await (_getByArgsWithEfOnPreValidateAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(args.Validate(nameof(args)).Entity().With>())
.Additional((__mv) => _getByArgsWithEfOnValidate?.Invoke(__mv, args, paging))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getByArgsWithEfOnBeforeAsync != null) await _getByArgsWithEfOnBeforeAsync(args, paging).ConfigureAwait(false);
+ await (_getByArgsWithEfOnBeforeAsync?.Invoke(args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetByArgsWithEfAsync(args, paging).ConfigureAwait(false);
- if (_getByArgsWithEfOnAfterAsync != null) await _getByArgsWithEfOnAfterAsync(__result, args, paging).ConfigureAwait(false);
+ await (_getByArgsWithEfOnAfterAsync?.Invoke(__result, args, paging) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -657,15 +646,15 @@ public async Task ThrowErrorAsync()
{
await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
- if (_throwErrorOnPreValidateAsync != null) await _throwErrorOnPreValidateAsync().ConfigureAwait(false);
+ await (_throwErrorOnPreValidateAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _throwErrorOnValidate?.Invoke(__mv))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_throwErrorOnBeforeAsync != null) await _throwErrorOnBeforeAsync().ConfigureAwait(false);
+ await (_throwErrorOnBeforeAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
await _dataService.ThrowErrorAsync().ConfigureAwait(false);
- if (_throwErrorOnAfterAsync != null) await _throwErrorOnAfterAsync().ConfigureAwait(false);
+ await (_throwErrorOnAfterAsync?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}, new BusinessInvokerArgs { IncludeTransactionScope = true, OperationType = OperationType.Unspecified }).ConfigureAwait(false);
}
@@ -679,15 +668,15 @@ await ManagerInvoker.Current.InvokeAsync(this, async () =>
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_invokeApiViaAgentOnPreValidateAsync != null) await _invokeApiViaAgentOnPreValidateAsync(id).ConfigureAwait(false);
+ await (_invokeApiViaAgentOnPreValidateAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Additional((__mv) => _invokeApiViaAgentOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_invokeApiViaAgentOnBeforeAsync != null) await _invokeApiViaAgentOnBeforeAsync(id).ConfigureAwait(false);
+ await (_invokeApiViaAgentOnBeforeAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.InvokeApiViaAgentAsync(id).ConfigureAwait(false);
- if (_invokeApiViaAgentOnAfterAsync != null) await _invokeApiViaAgentOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_invokeApiViaAgentOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Unspecified).ConfigureAwait(false);
}
@@ -702,16 +691,16 @@ await ManagerInvoker.Current.InvokeAsync(this, async () =>
return await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_getWithEfOnPreValidateAsync != null) await _getWithEfOnPreValidateAsync(id).ConfigureAwait(false);
+ await (_getWithEfOnPreValidateAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(id.Validate(nameof(id)).Mandatory())
.Additional((__mv) => _getWithEfOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_getWithEfOnBeforeAsync != null) await _getWithEfOnBeforeAsync(id).ConfigureAwait(false);
+ await (_getWithEfOnBeforeAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.GetWithEfAsync(id).ConfigureAwait(false);
- if (_getWithEfOnAfterAsync != null) await _getWithEfOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_getWithEfOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Read).ConfigureAwait(false);
}
@@ -729,16 +718,16 @@ public async Task CreateWithEfAsync(Person value)
{
value.Id = await _guidIdGen.GenerateIdentifierAsync().ConfigureAwait(false);
Cleaner.CleanUp(value);
- if (_createWithEfOnPreValidateAsync != null) await _createWithEfOnPreValidateAsync(value).ConfigureAwait(false);
+ await (_createWithEfOnPreValidateAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _createWithEfOnValidate?.Invoke(__mv, value))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_createWithEfOnBeforeAsync != null) await _createWithEfOnBeforeAsync(value).ConfigureAwait(false);
+ await (_createWithEfOnBeforeAsync?.Invoke(value) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.CreateWithEfAsync(value).ConfigureAwait(false);
- if (_createWithEfOnAfterAsync != null) await _createWithEfOnAfterAsync(__result).ConfigureAwait(false);
+ await (_createWithEfOnAfterAsync?.Invoke(__result) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Create).ConfigureAwait(false);
}
@@ -757,16 +746,16 @@ public async Task UpdateWithEfAsync(Person value, Guid id)
{
value.Id = id;
Cleaner.CleanUp(value);
- if (_updateWithEfOnPreValidateAsync != null) await _updateWithEfOnPreValidateAsync(value, id).ConfigureAwait(false);
+ await (_updateWithEfOnPreValidateAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(value.Validate(nameof(value)).Entity().With>())
.Additional((__mv) => _updateWithEfOnValidate?.Invoke(__mv, value, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_updateWithEfOnBeforeAsync != null) await _updateWithEfOnBeforeAsync(value, id).ConfigureAwait(false);
+ await (_updateWithEfOnBeforeAsync?.Invoke(value, id) ?? Task.CompletedTask).ConfigureAwait(false);
var __result = await _dataService.UpdateWithEfAsync(value).ConfigureAwait(false);
- if (_updateWithEfOnAfterAsync != null) await _updateWithEfOnAfterAsync(__result, id).ConfigureAwait(false);
+ await (_updateWithEfOnAfterAsync?.Invoke(__result, id) ?? Task.CompletedTask).ConfigureAwait(false);
return Cleaner.Clean(__result);
}, BusinessInvokerArgs.Update).ConfigureAwait(false);
}
@@ -780,16 +769,16 @@ public async Task DeleteWithEfAsync(Guid id)
await ManagerInvoker.Current.InvokeAsync(this, async () =>
{
Cleaner.CleanUp(id);
- if (_deleteWithEfOnPreValidateAsync != null) await _deleteWithEfOnPreValidateAsync(id).ConfigureAwait(false);
+ await (_deleteWithEfOnPreValidateAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
(await MultiValidator.Create()
.Add(id.Validate(nameof(id)).Mandatory())
.Additional((__mv) => _deleteWithEfOnValidate?.Invoke(__mv, id))
.RunAsync().ConfigureAwait(false)).ThrowOnError();
- if (_deleteWithEfOnBeforeAsync != null) await _deleteWithEfOnBeforeAsync(id).ConfigureAwait(false);
+ await (_deleteWithEfOnBeforeAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
await _dataService.DeleteWithEfAsync(id).ConfigureAwait(false);
- if (_deleteWithEfOnAfterAsync != null) await _deleteWithEfOnAfterAsync(id).ConfigureAwait(false);
+ await (_deleteWithEfOnAfterAsync?.Invoke(id) ?? Task.CompletedTask).ConfigureAwait(false);
}, BusinessInvokerArgs.Delete).ConfigureAwait(false);
}
}
diff --git a/samples/Demo/Beef.Demo.Cdc/Data/Generated/ContactCdcData.cs b/samples/Demo/Beef.Demo.Cdc/Data/Generated/ContactCdcData.cs
index 717fffb72..d6a64c771 100644
--- a/samples/Demo/Beef.Demo.Cdc/Data/Generated/ContactCdcData.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Data/Generated/ContactCdcData.cs
@@ -69,20 +69,30 @@ protected override async Task
- /// Gets the format.
+ /// Gets the (to be further formatted as per ).
///
- protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameAndKey;
+ protected override string EventSubject => "Legacy.Contact";
///
- /// Gets the (to be further formatted as per ).
+ /// Gets the .
///
- protected override string EventSubject => "Legacy.Contact";
+ protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameAndKey;
///
- /// Gets the .
+ /// Gets the .
///
protected override EventActionFormat EventActionFormat => EventActionFormat.PastTense;
+ ///
+ /// Gets the .
+ ///
+ protected override Uri? EventSource => new Uri("/cdc/contact", UriKind.Relative);
+
+ ///
+ /// Gets the .
+ ///
+ protected override EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.NameAndKey;
+
///
/// Gets the list of property names that should be excluded from the serialized JSON generation.
///
diff --git a/samples/Demo/Beef.Demo.Cdc/Data/Generated/Person2CdcData.cs b/samples/Demo/Beef.Demo.Cdc/Data/Generated/Person2CdcData.cs
index 294d292f5..1ccf4a6a5 100644
--- a/samples/Demo/Beef.Demo.Cdc/Data/Generated/Person2CdcData.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Data/Generated/Person2CdcData.cs
@@ -59,20 +59,30 @@ protected override async Task
- /// Gets the format.
+ /// Gets the (to be further formatted as per ).
///
- protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameAndKey;
+ protected override string EventSubject => "Demo.Cdc.Person2";
///
- /// Gets the (to be further formatted as per ).
+ /// Gets the .
///
- protected override string EventSubject => "Demo.Cdc.Person2";
+ protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameOnly;
///
- /// Gets the .
+ /// Gets the .
///
protected override EventActionFormat EventActionFormat => EventActionFormat.PastTense;
+ ///
+ /// Gets the .
+ ///
+ protected override Uri? EventSource => new Uri("/cdc/person2", UriKind.Relative);
+
+ ///
+ /// Gets the .
+ ///
+ protected override EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.NameAndKey;
+
///
/// Represents a wrapper to append the required (additional) database properties.
///
diff --git a/samples/Demo/Beef.Demo.Cdc/Data/Generated/PersonCdcData.cs b/samples/Demo/Beef.Demo.Cdc/Data/Generated/PersonCdcData.cs
index 00ef30d4c..e37a3c42c 100644
--- a/samples/Demo/Beef.Demo.Cdc/Data/Generated/PersonCdcData.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Data/Generated/PersonCdcData.cs
@@ -66,20 +66,30 @@ protected override async Task
- /// Gets the format.
+ /// Gets the (to be further formatted as per ).
///
- protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameAndKey;
+ protected override string EventSubject => "Demo.Cdc.Person";
///
- /// Gets the (to be further formatted as per ).
+ /// Gets the .
///
- protected override string EventSubject => "Demo.Cdc.Person";
+ protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameOnly;
///
- /// Gets the .
+ /// Gets the .
///
protected override EventActionFormat EventActionFormat => EventActionFormat.PastTense;
+ ///
+ /// Gets the .
+ ///
+ protected override Uri? EventSource => new Uri("/cdc/person", UriKind.Relative);
+
+ ///
+ /// Gets the .
+ ///
+ protected override EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.NameAndKey;
+
///
/// Represents a wrapper to append the required (additional) database properties.
///
diff --git a/samples/Demo/Beef.Demo.Cdc/Data/Generated/PostsCdcData.cs b/samples/Demo/Beef.Demo.Cdc/Data/Generated/PostsCdcData.cs
index 5dff33e00..67bf38718 100644
--- a/samples/Demo/Beef.Demo.Cdc/Data/Generated/PostsCdcData.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Data/Generated/PostsCdcData.cs
@@ -87,20 +87,30 @@ protected override async Task
- /// Gets the format.
+ /// Gets the (to be further formatted as per ).
///
- protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameOnly;
+ protected override string EventSubject => "Legacy.Post";
///
- /// Gets the (to be further formatted as per ).
+ /// Gets the .
///
- protected override string EventSubject => "Legacy.Post";
+ protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.NameOnly;
///
- /// Gets the .
+ /// Gets the .
///
protected override EventActionFormat EventActionFormat => EventActionFormat.PastTense;
+ ///
+ /// Gets the .
+ ///
+ protected override Uri? EventSource => new Uri("/cdc/posts", UriKind.Relative);
+
+ ///
+ /// Gets the .
+ ///
+ protected override EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.NameAndKey;
+
///
/// Represents a wrapper to append the required (additional) database properties.
///
diff --git a/samples/Demo/Beef.Demo.Cdc/Entities/Generated/ContactCdc.cs b/samples/Demo/Beef.Demo.Cdc/Entities/Generated/ContactCdc.cs
index 2c484023c..e0625c2d8 100644
--- a/samples/Demo/Beef.Demo.Cdc/Entities/Generated/ContactCdc.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Entities/Generated/ContactCdc.cs
@@ -136,7 +136,7 @@ public partial class ContactCdc : ITableKey, IETag, IGlobalIdentifier, ICdcLinkI
/// The .
public async Task LinkIdentifierMappingsAsync(CdcValueIdentifierMappingCollection coll, IStringIdentifierGenerator idGen)
{
- coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "Legacy", Table = "Contact", Key = this.CreateFormattedKey(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
+ coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "Legacy", Table = "Contact", Key = this.CreateIdentifierMappingKey(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
coll.AddAsync(GlobalAlternateContactId == default && AlternateContactId != default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalAlternateContactId), Schema = "Legacy", Table = "Contact", Key = AlternateContactId.ToString(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
await (Address?.LinkIdentifierMappingsAsync(coll, idGen) ?? Task.CompletedTask).ConfigureAwait(false);
}
@@ -231,7 +231,7 @@ public partial class AddressCdc : IUniqueKey, IGlobalIdentifier
/// The .
public async Task LinkIdentifierMappingsAsync(CdcValueIdentifierMappingCollection coll, IStringIdentifierGenerator idGen)
{
- coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "Legacy", Table = "Address", Key = this.CreateFormattedKey(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
+ coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "Legacy", Table = "Address", Key = this.CreateIdentifierMappingKey(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
coll.AddAsync(GlobalAlternateAddressId == default && AlternateAddressId != default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalAlternateAddressId), Schema = "Legacy", Table = "Address", Key = AlternateAddressId.ToString(), GlobalId = await idGen.GenerateIdentifierAsync().ConfigureAwait(false) });
}
diff --git a/samples/Demo/Beef.Demo.Cdc/Program.cs b/samples/Demo/Beef.Demo.Cdc/Program.cs
index 22f8ff9cb..e9a68c27d 100644
--- a/samples/Demo/Beef.Demo.Cdc/Program.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Program.cs
@@ -58,10 +58,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
services.AddBeefLoggerEventPublisher();
services.AddGeneratedCdcDataServices();
-
- services.AddCdcHostedService(hostContext.Configuration);
- services.AddCdcHostedService(hostContext.Configuration);
- services.AddCdcHostedService(hostContext.Configuration);
+ services.AddGeneratedCdcHostedServices(hostContext.Configuration);
});
}
}
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.Cdc/Services/Generated/CdcHostedServiceExtensions.cs b/samples/Demo/Beef.Demo.Cdc/Services/Generated/CdcHostedServiceExtensions.cs
new file mode 100644
index 000000000..838d2c46d
--- /dev/null
+++ b/samples/Demo/Beef.Demo.Cdc/Services/Generated/CdcHostedServiceExtensions.cs
@@ -0,0 +1,36 @@
+/*
+ * This file is automatically generated; any changes will be lost.
+ */
+
+#nullable enable
+#pragma warning disable
+
+using Beef.Data.Database.Cdc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Beef.Demo.Cdc.Services
+{
+ ///
+ /// Provides the generated extensions.
+ ///
+ public static class CdcHostedServiceExtensions
+ {
+ ///
+ /// Adds the generated CDC hosted services.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ public static IServiceCollection AddGeneratedCdcHostedServices(this IServiceCollection services, IConfiguration config)
+ {
+ services.AddCdcHostedService(config);
+ services.AddCdcHostedService(config);
+ services.AddCdcHostedService(config);
+ return services;
+ }
+ }
+}
+
+#pragma warning restore
+#nullable restore
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcBackgroundService.cs b/samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcHostedService.cs
similarity index 65%
rename from samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcBackgroundService.cs
rename to samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcHostedService.cs
index 9cdd6c317..54b9ec54a 100644
--- a/samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcBackgroundService.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcHostedService.cs
@@ -15,17 +15,17 @@
namespace Beef.Demo.Cdc.Services
{
///
- /// Provides the CDC background service for database object 'Legacy.Contact'.
+ /// Provides the capabilities for database object 'Legacy.Contact'.
///
- public partial class ContactCdcBackgroundService : CdcBackgroundService
+ public partial class ContactCdcHostedService : CdcHostedService
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public ContactCdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
+ public ContactCdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
}
}
diff --git a/samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcBackgroundService.cs b/samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcHostedService.cs
similarity index 65%
rename from samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcBackgroundService.cs
rename to samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcHostedService.cs
index 13fa72352..794023927 100644
--- a/samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcBackgroundService.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Services/Generated/PersonCdcHostedService.cs
@@ -15,17 +15,17 @@
namespace Beef.Demo.Cdc.Services
{
///
- /// Provides the CDC background service for database object 'Demo.Person'.
+ /// Provides the capabilities for database object 'Demo.Person'.
///
- public partial class PersonCdcBackgroundService : CdcBackgroundService
+ public partial class PersonCdcHostedService : CdcHostedService
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public PersonCdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
+ public PersonCdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
}
}
diff --git a/samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcBackgroundService.cs b/samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcHostedService.cs
similarity index 65%
rename from samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcBackgroundService.cs
rename to samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcHostedService.cs
index 5a38c1a5a..1d0e7063e 100644
--- a/samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcBackgroundService.cs
+++ b/samples/Demo/Beef.Demo.Cdc/Services/Generated/PostsCdcHostedService.cs
@@ -15,17 +15,17 @@
namespace Beef.Demo.Cdc.Services
{
///
- /// Provides the CDC background service for database object 'Legacy.Posts'.
+ /// Provides the capabilities for database object 'Legacy.Posts'.
///
- public partial class PostsCdcBackgroundService : CdcBackgroundService
+ public partial class PostsCdcHostedService : CdcHostedService
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public PostsCdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
+ public PostsCdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
}
}
diff --git a/samples/Demo/Beef.Demo.CodeGen/Beef.Demo.xml b/samples/Demo/Beef.Demo.CodeGen/Beef.Demo.xml
index c0006a282..3700869b0 100644
--- a/samples/Demo/Beef.Demo.CodeGen/Beef.Demo.xml
+++ b/samples/Demo/Beef.Demo.CodeGen/Beef.Demo.xml
@@ -1,7 +1,7 @@
-
+
-
+
@@ -12,8 +12,8 @@
-
-
+
+
@@ -119,7 +119,7 @@
-
+
diff --git a/samples/Demo/Beef.Demo.CodeGen/Properties/launchSettings.json b/samples/Demo/Beef.Demo.CodeGen/Properties/launchSettings.json
index 2dab624d7..eff4d367c 100644
--- a/samples/Demo/Beef.Demo.CodeGen/Properties/launchSettings.json
+++ b/samples/Demo/Beef.Demo.CodeGen/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"Beef.Demo.CodeGen": {
"commandName": "Project",
- "commandLineArgs": "entity",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\samples\\Demo\\Beef.Demo.CodeGen"
+ "commandLineArgs": "entity"
}
}
}
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.Common/Agents/Generated/DemoWebApiAgentArgs.cs b/samples/Demo/Beef.Demo.Common/Agents/Generated/DemoWebApiAgentArgs.cs
index 2baaae899..5d35e3f99 100644
--- a/samples/Demo/Beef.Demo.Common/Agents/Generated/DemoWebApiAgentArgs.cs
+++ b/samples/Demo/Beef.Demo.Common/Agents/Generated/DemoWebApiAgentArgs.cs
@@ -8,6 +8,7 @@
using Beef.WebApi;
using System;
using System.Net.Http;
+using System.Threading.Tasks;
namespace Beef.Demo.Common.Agents
{
@@ -26,7 +27,8 @@ public class DemoWebApiAgentArgs : WebApiAgentArgs, IDemoWebApiAgentArgs
///
/// The .
/// The optional action.
- public DemoWebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null) : base(httpClient, beforeRequest) { }
+ /// The optional asynchronous function.
+ public DemoWebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null, Func? beforeRequestAsync = null) : base(httpClient, beforeRequest, beforeRequestAsync) { }
}
}
diff --git a/samples/Demo/Beef.Demo.Common/Entities/Generated/Person.cs b/samples/Demo/Beef.Demo.Common/Entities/Generated/Person.cs
index 37f12623e..d9a46ec71 100644
--- a/samples/Demo/Beef.Demo.Common/Entities/Generated/Person.cs
+++ b/samples/Demo/Beef.Demo.Common/Entities/Generated/Person.cs
@@ -47,6 +47,7 @@ public partial class Person : EntityBase, IGuidIdentifier, IUniqueKey, IPartitio
///
[JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Include)]
[Display(Name="Identifier")]
+ [System.Xml.Serialization.XmlElement("Id")]
public Guid Id
{
get => _id;
diff --git a/samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml b/samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml
index 15528787f..a47502f81 100644
--- a/samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml
+++ b/samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml
@@ -1,5 +1,5 @@
-
+
@@ -18,7 +18,7 @@
-
+
@@ -72,7 +72,7 @@
-
+
diff --git a/samples/Demo/Beef.Demo.Database/Migrations/20210111-163747-create-democdc-postsoutbox.sql b/samples/Demo/Beef.Demo.Database/Migrations/20210111-163747-create-democdc-postsoutbox.sql
index 7c6b353f2..1978479cc 100644
--- a/samples/Demo/Beef.Demo.Database/Migrations/20210111-163747-create-democdc-postsoutbox.sql
+++ b/samples/Demo/Beef.Demo.Database/Migrations/20210111-163747-create-democdc-postsoutbox.sql
@@ -5,14 +5,14 @@ CREATE TABLE [DemoCdc].[PostsOutbox] (
[OutboxId] INT IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED ([OutboxId] ASC),
[CreatedDate] DATETIME NOT NULL,
- [PostsMinLsn] BINARY(10) NOT NULL, -- Primary table: Legacy.Posts
- [PostsMaxLsn] BINARY(10) NOT NULL,
- [CommentsMinLsn] BINARY(10) NOT NULL, -- Related table: Legacy.Comments
- [CommentsMaxLsn] BINARY(10) NOT NULL,
- [CommentsTagsMinLsn] BINARY(10) NOT NULL, -- Related table: Legacy.Tags
- [CommentsTagsMaxLsn] BINARY(10) NOT NULL,
- [PostsTagsMinLsn] BINARY(10) NOT NULL, -- Related table: Legacy.Tags
- [PostsTagsMaxLsn] BINARY(10) NOT NULL,
+ [PostsMinLsn] BINARY(10) NULL, -- Primary table: Legacy.Posts
+ [PostsMaxLsn] BINARY(10) NULL,
+ [CommentsMinLsn] BINARY(10) NULL, -- Related table: Legacy.Comments
+ [CommentsMaxLsn] BINARY(10) NULL,
+ [CommentsTagsMinLsn] BINARY(10) NULL, -- Related table: Legacy.Tags
+ [CommentsTagsMaxLsn] BINARY(10) NULL,
+ [PostsTagsMinLsn] BINARY(10) NULL, -- Related table: Legacy.Tags
+ [PostsTagsMaxLsn] BINARY(10) NULL,
[IsComplete] BIT NOT NULL,
[CompletedDate] DATETIME NULL,
[HasDataLoss] BIT NOT NULL
diff --git a/samples/Demo/Beef.Demo.Database/Migrations/20210111-163812-create-democdc-contactoutbox.sql b/samples/Demo/Beef.Demo.Database/Migrations/20210111-163812-create-democdc-contactoutbox.sql
index 96cc59a98..bcf824fb7 100644
--- a/samples/Demo/Beef.Demo.Database/Migrations/20210111-163812-create-democdc-contactoutbox.sql
+++ b/samples/Demo/Beef.Demo.Database/Migrations/20210111-163812-create-democdc-contactoutbox.sql
@@ -5,10 +5,10 @@ CREATE TABLE [DemoCdc].[ContactOutbox] (
[OutboxId] INT IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED ([OutboxId] ASC),
[CreatedDate] DATETIME NOT NULL,
- [ContactMinLsn] BINARY(10) NOT NULL, -- Primary table: Legacy.Contact
- [ContactMaxLsn] BINARY(10) NOT NULL,
- [AddressMinLsn] BINARY(10) NOT NULL, -- Related table: Legacy.Address
- [AddressMaxLsn] BINARY(10) NOT NULL,
+ [ContactMinLsn] BINARY(10) NULL, -- Primary table: Legacy.Contact
+ [ContactMaxLsn] BINARY(10) NULL,
+ [AddressMinLsn] BINARY(10) NULL, -- Related table: Legacy.Address
+ [AddressMaxLsn] BINARY(10) NULL,
[IsComplete] BIT NOT NULL,
[CompletedDate] DATETIME NULL,
[HasDataLoss] BIT NOT NULL
diff --git a/samples/Demo/Beef.Demo.Database/Migrations/20210126-211344-create-democdc-personoutbox.sql b/samples/Demo/Beef.Demo.Database/Migrations/20210126-211344-create-democdc-personoutbox.sql
index 2d12b74e7..66adf47c6 100644
--- a/samples/Demo/Beef.Demo.Database/Migrations/20210126-211344-create-democdc-personoutbox.sql
+++ b/samples/Demo/Beef.Demo.Database/Migrations/20210126-211344-create-democdc-personoutbox.sql
@@ -5,8 +5,8 @@ CREATE TABLE [DemoCdc].[PersonOutbox] (
[OutboxId] INT IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED ([OutboxId] ASC),
[CreatedDate] DATETIME NOT NULL,
- [PersonMinLsn] BINARY(10) NOT NULL, -- Primary table: Demo.Person
- [PersonMaxLsn] BINARY(10) NOT NULL,
+ [PersonMinLsn] BINARY(10) NULL, -- Primary table: Demo.Person
+ [PersonMaxLsn] BINARY(10) NULL,
[IsComplete] BIT NOT NULL,
[CompletedDate] DATETIME NULL,
[HasDataLoss] BIT NOT NULL
diff --git a/samples/Demo/Beef.Demo.Database/Migrations/20210127-175235-create-democdc-person2outbox.sql b/samples/Demo/Beef.Demo.Database/Migrations/20210127-175235-create-democdc-person2outbox.sql
index 9e64b3d55..75f8cfb0c 100644
--- a/samples/Demo/Beef.Demo.Database/Migrations/20210127-175235-create-democdc-person2outbox.sql
+++ b/samples/Demo/Beef.Demo.Database/Migrations/20210127-175235-create-democdc-person2outbox.sql
@@ -5,8 +5,8 @@ CREATE TABLE [DemoCdc].[Person2Outbox] (
[OutboxId] INT IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED ([OutboxId] ASC),
[CreatedDate] DATETIME NOT NULL,
- [Person2MinLsn] BINARY(10) NOT NULL, -- Primary table: Demo.Person2
- [Person2MaxLsn] BINARY(10) NOT NULL,
+ [Person2MinLsn] BINARY(10) NULL, -- Primary table: Demo.Person2
+ [Person2MaxLsn] BINARY(10) NULL,
[IsComplete] BIT NOT NULL,
[CompletedDate] DATETIME NULL,
[HasDataLoss] BIT NOT NULL
diff --git a/samples/Demo/Beef.Demo.Database/Properties/launchSettings.json b/samples/Demo/Beef.Demo.Database/Properties/launchSettings.json
index 0712592fd..6ceade728 100644
--- a/samples/Demo/Beef.Demo.Database/Properties/launchSettings.json
+++ b/samples/Demo/Beef.Demo.Database/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"Beef.Demo.Database": {
"commandName": "Project",
- "commandLineArgs": "codegen",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\samples\\Demo\\Beef.Demo.Database"
+ "commandLineArgs": "codegen"
}
}
}
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.EventSend/Program.cs b/samples/Demo/Beef.Demo.EventSend/Program.cs
index 5f387ccaa..e2538d10d 100644
--- a/samples/Demo/Beef.Demo.EventSend/Program.cs
+++ b/samples/Demo/Beef.Demo.EventSend/Program.cs
@@ -1,4 +1,5 @@
using Beef.Entities;
+using Beef.Events;
using Beef.Events.EventHubs;
using Beef.Events.ServiceBus;
using System;
@@ -74,7 +75,7 @@ static async Task Main()
break;
case "6":
- var ed = ehp.CreateValueEvent("N", "Demo.Robot.1", "PowerSourceChange");
+ var ed = EventData.CreateValueEvent("N", "Demo.Robot.1", "PowerSourceChange");
ed.Key = new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
ed.PartitionKey = PartitionKeyGenerator.Generate(Guid.NewGuid());
ehp.Publish(ed);
@@ -107,7 +108,7 @@ static async Task Main()
break;
case "16":
- ed = sbs.CreateValueEvent("N", "Demo.Robot.1", "PowerSourceChange");
+ ed = EventData.CreateValueEvent("N", "Demo.Robot.1", "PowerSourceChange");
ed.Key = new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
ed.PartitionKey = PartitionKeyGenerator.Generate(Guid.NewGuid());
sbs.Publish(ed);
diff --git a/samples/Demo/Beef.Demo.Functions/Startup.cs b/samples/Demo/Beef.Demo.Functions/Startup.cs
index 18002a26a..2f2c247c7 100644
--- a/samples/Demo/Beef.Demo.Functions/Startup.cs
+++ b/samples/Demo/Beef.Demo.Functions/Startup.cs
@@ -1,4 +1,5 @@
-using Beef.Caching.Policy;
+using Azure.Messaging.EventHubs.Producer;
+using Beef.Caching.Policy;
using Beef.Demo.Business;
using Beef.Demo.Business.Data;
using Beef.Demo.Business.DataSvc;
@@ -60,7 +61,7 @@ public override void Configure(IFunctionsHostBuilder builder)
.AddSingleton();
// Add event publishing.
- builder.Services.AddBeefEventHubEventProducer(config.GetValue("EventHubConnectionString"));
+ builder.Services.AddBeefEventHubEventProducer(new EventHubProducerClient(config.GetValue("EventHubConnectionString")));
// Add logging.
builder.Services.AddLogging();
diff --git a/samples/Demo/Beef.Demo.Functions/Subscribers/PowerSourceChangeSubscriber.cs b/samples/Demo/Beef.Demo.Functions/Subscribers/PowerSourceChangeSubscriber.cs
index 03f97517e..7b73db804 100644
--- a/samples/Demo/Beef.Demo.Functions/Subscribers/PowerSourceChangeSubscriber.cs
+++ b/samples/Demo/Beef.Demo.Functions/Subscribers/PowerSourceChangeSubscriber.cs
@@ -20,26 +20,25 @@ public PowerSourceChangeSubscriber(IRobotManager mgr)
public override async Task ReceiveAsync(EventData @event)
{
- if (@event.Key is Guid id)
- {
- if (id == new Guid(88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
- throw new DivideByZeroException("The mystery 88 guid can't be divided by zero.");
+ var id = @event.KeyAsGuid;
+ if (!id.HasValue)
+ return Result.InvalidData($"Key '{@event.Key ?? "null"}' must be a GUID.", ResultHandling.ContinueWithAudit);
- var robot = await _mgr.GetAsync(id);
- if (robot == null)
- return Result.DataNotFound();
+ if (id == new Guid(88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
+ throw new DivideByZeroException("The mystery 88 guid can't be divided by zero.");
- robot.AcceptChanges();
- robot.PowerSource = @event.Value;
- if (robot.IsChanged)
- await _mgr.UpdateAsync(robot, id);
+ var robot = await _mgr.GetAsync(id.Value);
+ if (robot == null)
+ return Result.DataNotFound();
- Logger.LogInformation("A trace message to prove it works!");
+ robot.AcceptChanges();
+ robot.PowerSource = @event.Value;
+ if (robot.IsChanged)
+ await _mgr.UpdateAsync(robot, id.Value);
- return Result.Success();
- }
- else
- return Result.InvalidData($"Key '{@event.Key ?? "null"}' must be a GUID.", ResultHandling.ContinueWithAudit);
+ Logger.LogInformation("A trace message to prove it works!");
+
+ return Result.Success();
}
}
}
\ No newline at end of file
diff --git a/samples/Demo/Beef.Demo.Test/CdcTest.cs b/samples/Demo/Beef.Demo.Test/CdcTest.cs
index eb2098d78..26ea2dd0b 100644
--- a/samples/Demo/Beef.Demo.Test/CdcTest.cs
+++ b/samples/Demo/Beef.Demo.Test/CdcTest.cs
@@ -40,7 +40,7 @@ private static void WriteResult(CdcDataOrchestratorResult result)
TestContext.WriteLine();
foreach (var ed in result.Events)
{
- TestContext.WriteLine($"{ed.Subject} {ed.Action}");
+ TestContext.WriteLine($"Subject: {ed.Subject}, Action: {ed.Action}, Source: {ed.Source}");
TestContext.WriteLine(JsonConvert.SerializeObject(ed.GetValue(), Formatting.Indented));
}
}
diff --git a/samples/Demo/Beef.Demo.Test/PersonTest.cs b/samples/Demo/Beef.Demo.Test/PersonTest.cs
index 444a2621d..90041eca7 100644
--- a/samples/Demo/Beef.Demo.Test/PersonTest.cs
+++ b/samples/Demo/Beef.Demo.Test/PersonTest.cs
@@ -163,9 +163,10 @@ public void B140_Get_NotModified()
AgentTester.Test()
.ExpectStatusCode(HttpStatusCode.NotModified)
- .RunOverride(() => new PersonAgent(new DemoWebApiAgentArgs(AgentTester.GetHttpClient(), r =>
+ .RunOverride(() => new PersonAgent(new DemoWebApiAgentArgs(AgentTester.GetHttpClient(), beforeRequestAsync: async r =>
{
r.Headers.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"" + p.ETag + "\""));
+ await Task.CompletedTask;
})).GetAsync(3.ToGuid()));
}
diff --git a/samples/My.Hr/My.Hr.Api/Startup.cs b/samples/My.Hr/My.Hr.Api/Startup.cs
index bfc733ca4..490f3c740 100644
--- a/samples/My.Hr/My.Hr.Api/Startup.cs
+++ b/samples/My.Hr/My.Hr.Api/Startup.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Reflection;
+using Azure.Messaging.EventHubs.Producer;
using Beef;
using Beef.AspNetCore.WebApi;
using Beef.Caching.Policy;
@@ -77,7 +78,7 @@ public void ConfigureServices(IServiceCollection services)
// Add event publishing services.
var ehcs = _config.GetValue("EventHubConnectionString");
if (!string.IsNullOrEmpty(ehcs))
- services.AddBeefEventHubEventProducer(ehcs);
+ services.AddBeefEventHubEventProducer(new EventHubProducerClient(ehcs));
else
services.AddBeefNullEventPublisher();
diff --git a/samples/My.Hr/My.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs b/samples/My.Hr/My.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs
index 3167ca68d..bf7ab56c5 100644
--- a/samples/My.Hr/My.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs
+++ b/samples/My.Hr/My.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs
@@ -54,8 +54,7 @@ public EmployeeDataSvc(IEmployeeData data, IEventPublisher evtPub, IRequestCache
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -69,9 +68,8 @@ public Task CreateAsync(Employee value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"My.Hr.Employee.{__result.Id}", "Created").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"My.Hr.Employee.{_evtPub.FormatKey(__result)}", "Created").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -85,9 +83,8 @@ public Task UpdateAsync(Employee value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"My.Hr.Employee.{__result.Id}", "Updated").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"My.Hr.Employee.{_evtPub.FormatKey(__result)}", "Updated").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -100,7 +97,7 @@ public Task DeleteAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"My.Hr.Employee.{id}", "Deleted", id).SendAsync().ConfigureAwait(false);
+ await _evtPub.Publish($"My.Hr.Employee.{_evtPub.FormatKey(id)}", "Deleted", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
});
}
@@ -132,8 +129,7 @@ public Task TerminateAsync(TerminationDetail value, Guid id)
{
var __result = await _data.TerminateAsync(Check.NotNull(value, nameof(value)), id).ConfigureAwait(false);
await _evtPub.PublishValue(__result, $"My.Hr.Employee.{id}", "Terminated", id).SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ return _cache.SetAndReturnValue(__result);
});
}
}
diff --git a/samples/My.Hr/My.Hr.Business/DataSvc/Generated/PerformanceReviewDataSvc.cs b/samples/My.Hr/My.Hr.Business/DataSvc/Generated/PerformanceReviewDataSvc.cs
index 73b18e85a..72d2d380c 100644
--- a/samples/My.Hr/My.Hr.Business/DataSvc/Generated/PerformanceReviewDataSvc.cs
+++ b/samples/My.Hr/My.Hr.Business/DataSvc/Generated/PerformanceReviewDataSvc.cs
@@ -54,8 +54,7 @@ public PerformanceReviewDataSvc(IPerformanceReviewData data, IEventPublisher evt
return __val;
var __result = await _data.GetAsync(id).ConfigureAwait(false);
- _cache.SetValue(__key, __result);
- return __result;
+ return _cache.SetAndReturnValue(__key, __result);
});
}
@@ -84,9 +83,8 @@ public Task CreateAsync(PerformanceReview value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.CreateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"My.Hr.PerformanceReview.{__result.Id}", "Created").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"My.Hr.PerformanceReview.{_evtPub.FormatKey(__result)}", "Created").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -100,9 +98,8 @@ public Task UpdateAsync(PerformanceReview value)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
var __result = await _data.UpdateAsync(Check.NotNull(value, nameof(value))).ConfigureAwait(false);
- await _evtPub.PublishValue(__result, $"My.Hr.PerformanceReview.{__result.Id}", "Updated").SendAsync().ConfigureAwait(false);
- _cache.SetValue((__result as IUniqueKey).UniqueKey, __result);
- return __result;
+ await _evtPub.PublishValue(__result, $"My.Hr.PerformanceReview.{_evtPub.FormatKey(__result)}", "Updated").SendAsync().ConfigureAwait(false);
+ return _cache.SetAndReturnValue(__result);
});
}
@@ -115,7 +112,7 @@ public Task DeleteAsync(Guid id)
return DataSvcInvoker.Current.InvokeAsync(this, async () =>
{
await _data.DeleteAsync(id).ConfigureAwait(false);
- await _evtPub.Publish($"My.Hr.PerformanceReview.{id}", "Deleted", id).SendAsync().ConfigureAwait(false);
+ await _evtPub.PublishValue(new PerformanceReview { Id = id }, $"My.Hr.PerformanceReview.{_evtPub.FormatKey(id)}", "Deleted", id).SendAsync().ConfigureAwait(false);
_cache.Remove(new UniqueKey(id));
});
}
diff --git a/samples/My.Hr/My.Hr.CodeGen/.vscode/settings.json b/samples/My.Hr/My.Hr.CodeGen/.vscode/settings.json
new file mode 100644
index 000000000..7a73a41bf
--- /dev/null
+++ b/samples/My.Hr/My.Hr.CodeGen/.vscode/settings.json
@@ -0,0 +1,2 @@
+{
+}
\ No newline at end of file
diff --git a/samples/My.Hr/My.Hr.CodeGen/Properties/launchSettings.json b/samples/My.Hr/My.Hr.CodeGen/Properties/launchSettings.json
index 73fbf123e..905c32712 100644
--- a/samples/My.Hr/My.Hr.CodeGen/Properties/launchSettings.json
+++ b/samples/My.Hr/My.Hr.CodeGen/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"My.Hr.CodeGen": {
"commandName": "Project",
- "commandLineArgs": "entity",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\samples\\My.Hr\\My.Hr.CodeGen"
+ "commandLineArgs": "all"
}
}
}
\ No newline at end of file
diff --git a/samples/My.Hr/My.Hr.Common/Agents/Generated/HrWebApiAgentArgs.cs b/samples/My.Hr/My.Hr.Common/Agents/Generated/HrWebApiAgentArgs.cs
index 3664aa428..1fb6d1e67 100644
--- a/samples/My.Hr/My.Hr.Common/Agents/Generated/HrWebApiAgentArgs.cs
+++ b/samples/My.Hr/My.Hr.Common/Agents/Generated/HrWebApiAgentArgs.cs
@@ -8,6 +8,7 @@
using Beef.WebApi;
using System;
using System.Net.Http;
+using System.Threading.Tasks;
namespace My.Hr.Common.Agents
{
@@ -26,7 +27,8 @@ public class HrWebApiAgentArgs : WebApiAgentArgs, IHrWebApiAgentArgs
///
/// The .
/// The optional action.
- public HrWebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null) : base(httpClient, beforeRequest) { }
+ /// The optional asynchronous function.
+ public HrWebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null, Func? beforeRequestAsync = null) : base(httpClient, beforeRequest, beforeRequestAsync) { }
}
}
diff --git a/samples/My.Hr/My.Hr.Database/Properties/launchSettings.json b/samples/My.Hr/My.Hr.Database/Properties/launchSettings.json
index db4767c1d..66abbbeeb 100644
--- a/samples/My.Hr/My.Hr.Database/Properties/launchSettings.json
+++ b/samples/My.Hr/My.Hr.Database/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"My.Hr.Database": {
"commandName": "Project",
- "commandLineArgs": "codegen",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\samples\\My.Hr\\My.Hr.Database"
+ "commandLineArgs": "codegen"
}
}
}
\ No newline at end of file
diff --git a/src/Beef.Core/Beef.Core.csproj b/src/Beef.Core/Beef.Core.csproj
index 9df00cab6..6910976e3 100644
--- a/src/Beef.Core/Beef.Core.csproj
+++ b/src/Beef.Core/Beef.Core.csproj
@@ -3,7 +3,7 @@
netstandard2.1Beef
- 4.1.10
+ 4.1.11trueBeef DevelopersAvanade
diff --git a/src/Beef.Core/CHANGELOG.md b/src/Beef.Core/CHANGELOG.md
index ac11fb18a..d09a04f1a 100644
--- a/src/Beef.Core/CHANGELOG.md
+++ b/src/Beef.Core/CHANGELOG.md
@@ -2,6 +2,14 @@
Represents the **NuGet** versions.
+## v4.1.11
+- *Enhancement:* Added new `EventData.Source` as an `Uri` to define the event source. The `EventData.Create*`, `IEventPublisher.Create*` and `IEventPublisher.Publish*` methods have new overloads to support the source `Uri`. `EventPublisherBase` simplified as the `IEventPublisher` is the primary means to access all methods given Dependency Injection (DI) usage.
+- *Enhancement:* Changed `EventData` to inherit from `EventMetadata` to house all the properties; this enables separation of metadata from `EventData` as required.
+- *Enhancement:* Added `IEventPublisher.SubjectFormat` and `IEventPublisher.ActionFormat` to enable optional uppercase or lowercase formatting.
+- *Enhancement:* Added additional overloads and methods to `IRequestCache` to simplify usage (and code-gen output).
+- *Enhancement:* Added `IUniqueKey` support to `ReferenceDataBase`.
+- *Enhancement:* Added `BeforeRequestAsync` to `IWebApiAgentArgs` to support asynchronous scenarios.
+
## v4.1.10
- *Fixed:* Issue [121](https://github.com/Avanade/Beef/issues/121). `SlidingCachePolicy` sliding logic functions as expected.
diff --git a/src/Beef.Core/Caching/IRequestCache.cs b/src/Beef.Core/Caching/IRequestCache.cs
index 58cf13077..c06eaf359 100644
--- a/src/Beef.Core/Caching/IRequestCache.cs
+++ b/src/Beef.Core/Caching/IRequestCache.cs
@@ -11,7 +11,7 @@ namespace Beef.Caching
public interface IRequestCache
{
///
- /// Gets the cached value associated with the specified and key.
+ /// Gets the cached value associated with the specified and .
///
/// The value .
/// The key of the value to get.
@@ -20,7 +20,16 @@ public interface IRequestCache
bool TryGetValue(UniqueKey key, out T value);
///
- /// Sets (adds or overrides) the cache value for the specified and key.
+ /// Gets the cached value associated with the specified and .
+ ///
+ /// The value .
+ /// The key of the value to get.
+ /// The cached value where found; otherwise, the default value for the .
+ /// true where found; otherwise, false.
+ bool TryGetValue(IUniqueKey key, out T value) => TryGetValue((key ?? throw new ArgumentNullException(nameof(key))).UniqueKey, out value);
+
+ ///
+ /// Sets (adds or overrides) the cache value for the specified and .
///
/// The value .
/// The key of the value to set.
@@ -28,13 +37,46 @@ public interface IRequestCache
void SetValue(UniqueKey key, T value);
///
- /// Removes the cached value associated with the specified and key.
+ /// Sets (adds or overrides) the cache value for the specified and and returns .
+ ///
+ /// The value .
+ /// The key of the value to set.
+ /// The value to set.
+ /// The .
+ T? SetAndReturnValue(UniqueKey key, T? value)
+ {
+ SetValue(key, value);
+ return value;
+ }
+
+ ///
+ /// Sets (adds or overrides) the cache value for the specified and and reurns .
+ ///
+ /// The value .
+ /// The value to set.
+ /// The .
+ T? SetAndReturnValue(T? value) where T : IUniqueKey
+ {
+ SetValue((value ?? throw new ArgumentNullException(nameof(value))).UniqueKey, value);
+ return value;
+ }
+
+ ///
+ /// Removes the cached value associated with the specified and .
///
/// The value .
/// The key of the value to remove.
/// true where found and removed; otherwise, false.
bool Remove(UniqueKey key);
+ ///
+ /// Removes the cached value associated with the specified and .
+ ///
+ /// The value .
+ /// The key of the value to remove.
+ /// true where found and removed; otherwise, false.
+ bool Remove(IUniqueKey key) => Remove((key ?? throw new ArgumentNullException(nameof(key))).UniqueKey);
+
///
/// Clears the cache for the specified .
///
diff --git a/src/Beef.Core/Events/EventData.cs b/src/Beef.Core/Events/EventData.cs
index fd82c9454..f196ba448 100644
--- a/src/Beef.Core/Events/EventData.cs
+++ b/src/Beef.Core/Events/EventData.cs
@@ -7,97 +7,74 @@
namespace Beef.Events
{
///
- /// Represents the EventData.
+ /// Provides the Beef event-metadata and property names.
///
[JsonObject(MemberSerialization.OptIn)]
- public class EventData : IETag
+ public class EventMetadata : IETag
{
+ #region AttributeNames
+
///
- /// Creates an instance with no .
+ /// Gets or sets the EventId attribute name.
///
- /// The event subject.
- /// The event action.
- /// The .
- public static EventData CreateEvent(string subject, string? action = null)
- => new EventData { Subject = Check.NotEmpty(subject, nameof(subject)), Action = action };
+ public static string EventIdAttributeName { get; set; } = "Beef.EventId";
///
- /// Creates an instance with the specified .
+ /// Gets or sets the Subject attribute name.
///
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public static EventData CreateEvent(string subject, string? action = null, params IComparable?[] key)
- => new EventData { Subject = Check.NotEmpty(subject, nameof(subject)), Action = action, Key = key.Length == 1 ? (object?)key[0] : key };
+ public static string SubjectAttributeName { get; set; } = "Beef.Subject";
///
- /// Creates an instance using the (infers the from either or ).
+ /// Gets or sets the Action attribute name.
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The .
- public static EventData CreateValueEvent(T value, string subject, string? action = null) where T : class
- {
- var ed = new EventData { Value = Check.NotNull(value, nameof(value)) };
- ed.Subject = Check.NotEmpty(subject, nameof(subject));
- ed.Action = action;
+ public static string ActionAttributeName { get; set; } = "Beef.Action";
- switch (value)
- {
- case IIntIdentifier ii:
- ed.Key = ii.Id;
- break;
+ ///
+ /// Gets or sets the Source attribute name.
+ ///
+ public static string SourceAttributeName { get; set; } = "Beef.Source";
- case IGuidIdentifier gi:
- ed.Key = gi.Id;
- break;
+ ///
+ /// Gets or sets the TenantId attribute name.
+ ///
+ public static string TenantIdAttributeName { get; set; } = "Beef.TenantId";
- case IStringIdentifier si:
- ed.Key = si.Id;
- break;
+ ///
+ /// Gets or sets the Key attribute name.
+ ///
+ public static string KeyPropertyName { get; set; } = "Beef.Key";
- case IUniqueKey uk:
- ed.Key = uk.UniqueKey.Args.Length == 1 ? uk.UniqueKey.Args[0] : uk.UniqueKey.Args;
- break;
- }
+ ///
+ /// Gets or sets the ETag attribute name.
+ ///
+ public static string ETagAttributeName { get; set; } = "Beef.ETag";
- return ed;
- }
+ ///
+ /// Gets or sets the TenantId attribute name.
+ ///
+ public static string UsernameAttributeName { get; set; } = "Beef.Username";
///
- /// Creates an instance with the specified .
+ /// Gets or sets the TenantId attribute name.
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public static EventData CreateValueEvent(T value, string subject, string? action = null, params IComparable?[] key)
- => new EventData { Value = value, Subject = Check.NotEmpty(subject, nameof(subject)), Action = action, Key = key.Length == 1 ? (object?)key[0] : key };
+ public static string UserIdAttributeName { get; set; } = "Beef.UserId";
///
- /// Initializes a new instance of the class defaulting as applicable using the equivalent values.
+ /// Gets or sets the TenantId attribute name.
///
- public EventData()
- {
- EventId = Guid.NewGuid();
+ public static string TimestampAttributeName { get; set; } = "Beef.Timestamp";
- if (ExecutionContext.HasCurrent)
- {
- TenantId = ExecutionContext.Current.TenantId;
- Timestamp = ExecutionContext.Current.Timestamp;
- Username = ExecutionContext.Current.Username;
- UserId = ExecutionContext.Current.UserId;
- CorrelationId = ExecutionContext.Current.CorrelationId;
- PartitionKey = ExecutionContext.Current.TenantId?.ToString();
- }
- else
- Timestamp = Cleaner.Clean(DateTime.UtcNow);
- }
+ ///
+ /// Gets or sets the CorrelationId attribute name.
+ ///
+ public static string CorrelationIdAttributeName { get; set; } = "Beef.CorrelationId";
+
+ ///
+ /// Gets or sets the PartitionKey attribute name.
+ ///
+ public static string PartitionKeyAttributeName { get; set; } = "Beef.PartitionKey";
+
+ #endregion
///
/// Gets or sets the unique event identifier.
@@ -111,7 +88,7 @@ public EventData()
[JsonProperty("tenantId", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Guid? TenantId { get; set; }
- ///
+ ///
/// Gets or sets the event subject (the name should use the '.' character to denote paths).
///
[JsonProperty("subject", DefaultValueHandling = DefaultValueHandling.Ignore)]
@@ -123,6 +100,12 @@ public EventData()
[JsonProperty("action", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string? Action { get; set; }
+ ///
+ /// Gets or sets the event source (describes the event published/producer).
+ ///
+ [JsonProperty("source", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public Uri? Source { get; set; }
+
///
/// Gets or sets the entity key (could be single value or an array of values).
///
@@ -162,8 +145,228 @@ public EventData()
///
/// Gets or sets the partition key.
///
+ [JsonProperty("partitionKey", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string? PartitionKey { get; set; }
+ ///
+ /// Gets (converts) the to a . This is achieved by either checking it is a and returning as-is; otherwise, the value will be cast to a
+ /// and parsed.
+ ///
+ /// The where valid; otherwise, null.
+ public Guid? KeyAsGuid => Key == null ? null : (Key is Guid g ? g : (Guid.TryParse((string)Key, out var gx) ? gx : (Guid?)null));
+
+ ///
+ /// Gets (converts) the to a . This is achieved by either checking it is a and returning as-is; otherwise, the value will be cast to a
+ /// and parsed.
+ ///
+ /// The where valid; otherwise, null.
+ public int? KeyAsInt32 => Key == null ? null : (Key is int i ? i : (int.TryParse((string)Key, out var ix) ? ix : (int?)null));
+
+ ///
+ /// Gets (converts) the to a . This is achieved by either checking it is a and returning as-is; otherwise, the value will be cast to a
+ /// and parsed.
+ ///
+ /// The where valid; otherwise, null.
+ public long? KeyAsInt64 => Key == null ? null : (Key is long l ? l : (long.TryParse((string)Key, out var lx) ? lx : (long?)null));
+
+ ///
+ /// Gets (converts) the to a . This is achieved by either checking it is a and returning as-is; otherwise, the value will be cast to a
+ /// .
+ ///
+ /// The where valid; otherwise, null.
+ public string? KeyAsString => Key == null ? null : (Key is string s ? s : (string)Key);
+
+ ///
+ /// Creates (clones) a new instance copying the existing values.
+ ///
+ ///
+ public EventMetadata CopyMetadata() => new EventMetadata
+ {
+ EventId = EventId,
+ TenantId = TenantId,
+ Subject = Subject,
+ Action = Action,
+ Source = Source,
+ Key = Key,
+ ETag = ETag,
+ Username = Username,
+ UserId = UserId,
+ Timestamp = Timestamp,
+ CorrelationId = CorrelationId,
+ PartitionKey = PartitionKey
+ };
+ }
+
+ ///
+ /// Represents the EventData.
+ ///
+ [JsonObject(MemberSerialization.OptIn)]
+ public class EventData : EventMetadata
+ {
+ ///
+ /// Creates an instance with no .
+ ///
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateEvent(string subject, string? action = null)
+ => new() { Subject = Check.NotEmpty(subject, nameof(subject)), Action = action };
+
+ ///
+ /// Creates an instance with no .
+ ///
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateEvent(Uri source, string subject, string? action = null)
+ => new() { Source = Check.NotNull(source, nameof(source)), Subject = Check.NotEmpty(subject, nameof(subject)), Action = action };
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateEvent(string subject, string? action = null, params IComparable?[] key)
+ => new() { Subject = Check.NotEmpty(subject, nameof(subject)), Action = action, Key = key.Length == 1 ? (object?)key[0] : key };
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateEvent(Uri source, string subject, string? action = null, params IComparable?[] key)
+ => new() { Source = Check.NotNull(source, nameof(source)), Subject = Check.NotEmpty(subject, nameof(subject)), Action = action, Key = key.Length == 1 ? (object?)key[0] : key };
+
+ ///
+ /// Creates an instance using the (infers the from either or ).
+ ///
+ /// The value .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateValueEvent(T value, string subject, string? action = null) where T : class
+ {
+ var ed = new EventData { Value = Check.NotNull(value, nameof(value)) };
+ ed.Subject = Check.NotEmpty(subject, nameof(subject));
+ ed.Action = action;
+
+ switch (value)
+ {
+ case IIntIdentifier ii:
+ ed.Key = ii.Id;
+ break;
+
+ case IGuidIdentifier gi:
+ ed.Key = gi.Id;
+ break;
+
+ case IStringIdentifier si:
+ ed.Key = si.Id;
+ break;
+
+ case IUniqueKey uk:
+ ed.Key = uk.UniqueKey.Args.Length == 1 ? uk.UniqueKey.Args[0] : uk.UniqueKey.Args;
+ break;
+ }
+
+ return ed;
+ }
+
+ ///
+ /// Creates an instance using the (infers the from either or ).
+ ///
+ /// The value .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateValueEvent(T value, Uri source, string subject, string? action = null) where T : class
+ {
+ var ed = CreateValueEvent(value, subject, action);
+ ed.Source = Check.NotNull(source, nameof(source));
+ return ed;
+ }
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The value .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateValueEvent(T value, string subject, string? action = null, params IComparable?[] key)
+ => new() { Value = value, Subject = Check.NotEmpty(subject, nameof(subject)), Action = action, Key = key.Length == 1 ? (object?)key[0] : key };
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The value .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateValueEvent(T value, Uri source, string subject, string? action = null, params IComparable?[] key)
+ {
+ var ed = CreateValueEvent(value, subject, action, key);
+ ed.Source = Check.NotNull(source, nameof(source));
+ return ed;
+ }
+
+ ///
+ /// Initializes a new instance of the class defaulting as applicable using the equivalent values.
+ ///
+ public EventData()
+ {
+ EventId = Guid.NewGuid();
+
+ if (ExecutionContext.HasCurrent)
+ {
+ TenantId = ExecutionContext.Current.TenantId;
+ Timestamp = ExecutionContext.Current.Timestamp;
+ Username = ExecutionContext.Current.Username;
+ UserId = ExecutionContext.Current.UserId;
+ CorrelationId = ExecutionContext.Current.CorrelationId;
+ PartitionKey = ExecutionContext.Current.TenantId?.ToString();
+ }
+ else
+ Timestamp = Cleaner.Clean(DateTime.UtcNow);
+ }
+
+ ///
+ /// Initializes a new instance of the class from a .
+ ///
+ /// The .
+ public EventData(EventMetadata? metadata)
+ {
+ if (metadata == null)
+ return;
+
+ EventId = metadata.EventId;
+ TenantId = metadata.TenantId;
+ Subject = metadata.Subject;
+ Action = metadata.Action;
+ Source = metadata.Source;
+ Key = metadata.Key;
+ ETag = metadata.ETag;
+ Username = metadata.Username;
+ UserId = metadata.UserId;
+ Timestamp = metadata.Timestamp;
+ CorrelationId = metadata.CorrelationId;
+ PartitionKey = metadata.PartitionKey;
+ }
+
///
/// Resets the value to the default.
///
@@ -185,6 +388,35 @@ public virtual void ResetValue() { }
///
///
public virtual void SetValue(object? value) => throw new NotSupportedException();
+
+ ///
+ /// Merges in the updating any existing values where they are currently null.
+ ///
+ ///
+ public void MergeMetadata(EventMetadata metadata)
+ {
+ InvokeOnNull(EventId, () => EventId = metadata.EventId);
+ InvokeOnNull(TenantId, () => TenantId = metadata.TenantId);
+ InvokeOnNull(Subject, () => Subject = metadata.Subject);
+ InvokeOnNull(Action, () => Action = metadata.Action);
+ InvokeOnNull(Source, () => Source = metadata.Source);
+ InvokeOnNull(Key, () => Key = metadata.Key);
+ InvokeOnNull(ETag, () => ETag = metadata.ETag);
+ InvokeOnNull(Username, () => Username = metadata.Username);
+ InvokeOnNull(UserId, () => UserId = metadata.UserId);
+ InvokeOnNull(Timestamp, () => Timestamp = metadata.Timestamp);
+ InvokeOnNull(CorrelationId, () => CorrelationId = metadata.CorrelationId);
+ InvokeOnNull(PartitionKey, () => PartitionKey = metadata.PartitionKey);
+ }
+
+ ///
+ /// Invokes the action on null value.
+ ///
+ private static void InvokeOnNull(object? value, Action action)
+ {
+ if (value == null)
+ action();
+ }
}
///
@@ -196,7 +428,18 @@ public class EventData : EventData
private T _value = default!;
///
- /// Gets (same as ) or sets (same as ) the event value (automatically setting/overriding the and ).
+ /// Initializes a new instance of the class.
+ ///
+ public EventData() : base() { }
+
+ ///
+ /// Initializes a new instance of the class from a .
+ ///
+ /// The .
+ public EventData(EventMetadata? metadata) : base(metadata) { }
+
+ ///
+ /// Gets (same as ) or sets (same as ) the event value (automatically setting/overriding the and ).
///
[JsonProperty("value", DefaultValueHandling = DefaultValueHandling.Ignore)]
public T Value
diff --git a/src/Beef.Core/Events/EventExtensions.cs b/src/Beef.Core/Events/EventExtensions.cs
new file mode 100644
index 000000000..5c57a3ac3
--- /dev/null
+++ b/src/Beef.Core/Events/EventExtensions.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+#pragma warning disable IDE0060 // Remove unused parameter; for backwards compatibility.
+
+using Beef.Entities;
+using System;
+
+namespace Beef.Events
+{
+ ///
+ /// Event specific extension methods.
+ ///
+ public static class EventExtensions
+ {
+ ///
+ /// Creates an instance with no .
+ ///
+ /// The .
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateEvent(this IEventPublisher eventPublisher, string subject, string? action = null) => EventData.CreateEvent(subject, action);
+
+ ///
+ /// Creates an instance with no .
+ ///
+ /// The .
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateEvent(this IEventPublisher eventPublisher, Uri source, string subject, string? action = null) => EventData.CreateEvent(source, subject, action);
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The .
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateEvent(this IEventPublisher eventPublisher, string subject, string? action = null, params IComparable?[] key) => EventData.CreateEvent(subject, action, key);
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The .
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateEvent(this IEventPublisher eventPublisher, Uri source, string subject, string? action = null, params IComparable?[] key) => EventData.CreateEvent(source, subject, action, key);
+
+ ///
+ /// Creates an instance using the (infers the from either or ).
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateValueEvent(this IEventPublisher eventPublisher, T value, string subject, string? action = null) where T : class => EventData.CreateValueEvent(value, subject, action);
+
+ ///
+ /// Creates an instance using the (infers the from either or ).
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The .
+ public static EventData CreateValueEvent(this IEventPublisher eventPublisher, T value, Uri source, string subject, string? action = null) where T : class => EventData.CreateValueEvent(value, source, subject, action);
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateValueEvent(this IEventPublisher eventPublisher, T value, string subject, string? action = null, params IComparable?[] key) => EventData.CreateValueEvent(value, subject, action, key);
+
+ ///
+ /// Creates an instance with the specified .
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The .
+ public static EventData CreateValueEvent(this IEventPublisher eventPublisher, T value, Uri source, string subject, string? action = null, params IComparable?[] key) => EventData.CreateValueEvent(value, source, subject, action, key);
+
+ ///
+ /// Publishes (queues) an instance (with no ).
+ ///
+ /// The .
+ /// The event subject.
+ /// The event action.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher Publish(this IEventPublisher eventPublisher, string subject, string? action = null) => eventPublisher.Publish(eventPublisher.CreateEvent(subject, action));
+
+ ///
+ /// Publishes (queues) an instance (with no ).
+ ///
+ /// The .
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher Publish(this IEventPublisher eventPublisher, Uri source, string subject, string? action = null) => eventPublisher.Publish(eventPublisher.CreateEvent(source, subject, action));
+
+ ///
+ /// Publishes (queues) an instance using the specified .
+ ///
+ /// The .
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher Publish(this IEventPublisher eventPublisher, string subject, string? action = null, params IComparable?[] key) => eventPublisher.Publish(eventPublisher.CreateEvent(subject, action, key));
+
+ ///
+ /// Publishes (queues) an instance using the specified .
+ ///
+ /// The .
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher Publish(this IEventPublisher eventPublisher, Uri source, string subject, string? action = null, params IComparable?[] key) => eventPublisher.Publish(eventPublisher.CreateEvent(source, subject, action, key));
+
+ ///
+ /// Publishes (queues) an instance using the (infers ).
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher PublishValue(this IEventPublisher eventPublisher, T value, string subject, string? action = null) where T : class => eventPublisher.Publish(eventPublisher.CreateValueEvent(value, subject, action));
+
+ ///
+ /// Publishes (queues) an instance using the (infers ).
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher PublishValue(this IEventPublisher eventPublisher, T value, Uri source, string subject, string? action = null) where T : class => eventPublisher.Publish(eventPublisher.CreateValueEvent(value, source, subject, action));
+
+ ///
+ /// Publishes (queues) an instance using the specified .
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher PublishValue(this IEventPublisher eventPublisher, T value, string subject, string? action = null, params IComparable?[] key) => eventPublisher.Publish(eventPublisher.CreateValueEvent(value, subject, action, key));
+
+ ///
+ /// Publishes (queues) an instance using the specified .
+ ///
+ /// The value .
+ /// The .
+ /// The event value
+ /// The event source.
+ /// The event subject.
+ /// The event action.
+ /// The event key.
+ /// The for fluent-style method-chaining.
+ public static IEventPublisher PublishValue(this IEventPublisher eventPublisher, T value, Uri source, string subject, string? action = null, params IComparable?[] key) => eventPublisher.Publish(eventPublisher.CreateValueEvent(value, source, subject, action, key));
+ }
+}
+
+#pragma warning restore IDE0060
\ No newline at end of file
diff --git a/src/Beef.Core/Events/EventPublisherBase.cs b/src/Beef.Core/Events/EventPublisherBase.cs
index 877b881c5..44b1180e6 100644
--- a/src/Beef.Core/Events/EventPublisherBase.cs
+++ b/src/Beef.Core/Events/EventPublisherBase.cs
@@ -1,6 +1,5 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-using Beef.Entities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -15,12 +14,12 @@ namespace Beef.Events
/// The key reason for queuing the published events it to promote a single atomic send operation; i.e. all events should be sent together, and either succeed or fail together.
public abstract class EventPublisherBase : IEventPublisher
{
- private readonly Lazy> _queue = new Lazy>();
+ private readonly Lazy> _queue = new();
///
- /// Gets or sets the prefix for an when creating an or . Note: the will automatically be applied.
+ /// Gets or sets the default source to be used where not otherwise specified.
///
- public string? EventSubjectPrefix { get; set; }
+ public Uri? DefaultSource { get; set; }
///
/// Gets or sets the path seperator .
@@ -32,6 +31,16 @@ public abstract class EventPublisherBase : IEventPublisher
///
public string TemplateWildcard { get; set; } = "*";
+ ///
+ /// Gets or sets the format.
+ ///
+ public EventStringFormat SubjectFormat { get; set; } = EventStringFormat.None;
+
+ ///
+ /// Gets or sets the format.
+ ///
+ public EventStringFormat ActionFormat { get; set; } = EventStringFormat.None;
+
///
/// Gets the published/queued events.
///
@@ -48,107 +57,36 @@ public EventData[] GetEvents()
return list.ToArray();
}
- ///
- /// Prepends the to the where specified before creating the .
- ///
- protected virtual string PrependPrefix(string subject) => string.IsNullOrEmpty(EventSubjectPrefix) ? subject : EventSubjectPrefix + PathSeparator + subject;
-
- ///
- /// Creates an instance with no .
- ///
- /// The event subject.
- /// The event action.
- /// The .
- public EventData CreateEvent(string subject, string? action = null)
- => EventData.CreateEvent(PrependPrefix(Check.NotEmpty(subject, nameof(subject))), action);
-
- ///
- /// Creates an instance with the specified .
- ///
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public EventData CreateEvent(string subject, string? action = null, params IComparable?[] key)
- => EventData.CreateEvent(PrependPrefix(Check.NotEmpty(subject, nameof(subject))), action, key);
-
- ///
- /// Creates an instance using the (infers the from either or ).
- ///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The .
- public EventData CreateValueEvent(T value, string subject, string? action = null) where T : class
- => EventData.CreateValueEvent(value, PrependPrefix(Check.NotEmpty(subject, nameof(subject))), action);
-
- ///
- /// Creates an instance with the specified .
- ///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public EventData CreateValueEvent(T value, string subject, string? action = null, params IComparable?[] key)
- => EventData.CreateValueEvent(value, PrependPrefix(Check.NotEmpty(subject, nameof(subject))), action, key);
-
- ///
- /// Publishes (queues) an instance (with no ).
- ///
- /// The event subject.
- /// The event action.
- /// The .
- public IEventPublisher Publish(string subject, string? action = null) => Publish(new EventData[] { CreateEvent(subject, action) });
-
- ///
- /// Publishes (queues) an instance using the specified .
- ///
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public IEventPublisher Publish(string subject, string? action = null, params IComparable?[] key) => Publish(new EventData[] { CreateEvent(subject, action, key) });
-
- ///
- /// Publishes an instance using the (infers ).
- ///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The .
- public IEventPublisher PublishValue(T value, string subject, string? action = null) where T : class => Publish(new EventData[] { CreateValueEvent(value, subject, action) });
-
- ///
- /// Publishes (queues) an instance using the specified .
- ///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- public IEventPublisher PublishValue(T value, string subject, string? action = null, params IComparable?[] key) => Publish(new EventData[] { CreateValueEvent(value, subject, action, key) });
-
///
/// Publishes (queues) one of more objects.
///
/// One or more objects.
/// The .
+ /// This method will also perform the appropriate and on the .
public virtual IEventPublisher Publish(params EventData[] events)
{
Check.IsFalse(events.Any(x => string.IsNullOrEmpty(x.Subject)), nameof(events), "EventData must have a Subject.");
foreach (var ed in events)
{
+ ed.Subject = Format(Check.NotEmpty(ed.Subject, nameof(EventData.Subject)), SubjectFormat);
+ ed.Action = Format(ed.Action, ActionFormat);
+ ed.Source ??= DefaultSource;
_queue.Value.Enqueue(ed);
}
return this;
}
+ ///
+ /// Format the string.
+ ///
+ private static string? Format(string? text, EventStringFormat? format) => string.IsNullOrEmpty(text) ? text : format switch
+ {
+ EventStringFormat.Uppercase => text.ToUpperInvariant()!,
+ EventStringFormat.Lowercase => text.ToLowerInvariant()!,
+ _ => text
+ };
+
///
/// Sends all previously (queued) published events.
///
diff --git a/src/Beef.Core/Events/EventStringFormat.cs b/src/Beef.Core/Events/EventStringFormat.cs
new file mode 100644
index 000000000..8300dbecc
--- /dev/null
+++ b/src/Beef.Core/Events/EventStringFormat.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+namespace Beef.Events
+{
+ ///
+ /// Defines the event string format.
+ ///
+ public enum EventStringFormat
+ {
+ ///
+ /// No formatting; as-is (default).
+ ///
+ None,
+
+ ///
+ /// Format as upper case.
+ ///
+ Uppercase,
+
+ ///
+ /// Format as lower case.
+ ///
+ Lowercase
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Core/Events/IEventPublisher.cs b/src/Beef.Core/Events/IEventPublisher.cs
index 41a8ab3ff..4c0dd520e 100644
--- a/src/Beef.Core/Events/IEventPublisher.cs
+++ b/src/Beef.Core/Events/IEventPublisher.cs
@@ -2,6 +2,7 @@
using Beef.Entities;
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace Beef.Events
@@ -14,9 +15,9 @@ namespace Beef.Events
public interface IEventPublisher
{
///
- /// Gets the prefix for an when creating an or . Note: the will automatically be applied.
+ /// Gets the default source to be used where not otherwise specified (see ).
///
- string? EventSubjectPrefix { get; }
+ Uri? DefaultSource { get; }
///
/// Gets the path seperator .
@@ -24,98 +25,67 @@ public interface IEventPublisher
string PathSeparator { get; }
///
- /// Gets the template wildcard .
- ///
- string TemplateWildcard { get; }
-
- ///
- /// Gets the published/queued events.
+ /// Gets the key seperator .
///
- /// An array.
- EventData[] GetEvents();
+ string KeySeparator => ",";
///
- /// Creates an instance with no .
- ///
- /// The event subject.
- /// The event action.
- /// The .
- EventData CreateEvent(string subject, string? action = null);
-
- ///
- /// Creates an instance with the specified .
- ///
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- EventData CreateEvent(string subject, string? action = null, params IComparable?[] key);
-
- ///
- /// Creates an instance using the (infers the from either or ).
+ /// Gets the template wildcard .
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The .
- EventData CreateValueEvent(T value, string subject, string? action = null) where T : class;
+ string TemplateWildcard { get; }
///
- /// Creates an instance with the specified .
+ /// Gets the format.
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The .
- EventData CreateValueEvent(T value, string subject, string? action = null, params IComparable?[] key);
+ EventStringFormat SubjectFormat => EventStringFormat.None;
///
- /// Publishes (queues) an instance (with no ).
+ /// Gets the format.
///
- /// The event subject.
- /// The event action.
- /// The for fluent-style method-chaining.
- IEventPublisher Publish(string subject, string? action = null);
+ EventStringFormat ActionFormat => EventStringFormat.None;
///
- /// Publishes (queues) an instance using the specified .
+ /// Gets the published/queued events.
///
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The for fluent-style method-chaining.
- IEventPublisher Publish(string subject, string? action = null, params IComparable?[] key);
+ /// An array.
+ EventData[] GetEvents();
///
- /// Publishes (queues) an instance using the (infers ).
+ /// Publishes (queues) one of more objects.
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
+ /// One or more objects.
/// The for fluent-style method-chaining.
- IEventPublisher PublishValue(T value, string subject, string? action = null) where T : class;
+ IEventPublisher Publish(params EventData[] events);
///
- /// Publishes (queues) an instance using the specified .
+ /// Formats a as its formatted key representation.
///
- /// The value .
- /// The event value
- /// The event subject.
- /// The event action.
- /// The event key.
- /// The for fluent-style method-chaining.
- IEventPublisher PublishValue(T value, string subject, string? action = null, params IComparable?[] key);
+ /// The value.
+ /// The formatted key.
+ string? FormatKey(object? value)
+ {
+ if (value == null)
+ return null;
+
+ if (value is string s)
+ return s;
+
+ return value switch
+ {
+ IIntIdentifier ii => ii.Id.ToString(),
+ IGuidIdentifier gi => gi.Id.ToString(),
+ IStringIdentifier si => si.Id,
+ IUniqueKey uk => uk.UniqueKey.Args.Length == 1 ? FormatKey(uk.UniqueKey.Args[0]) : FormatKey(uk.UniqueKey.Args),
+ _ => value.ToString(),
+ };
+ }
///
- /// Publishes (queues) one of more objects.
+ /// Formats one or more as its formatted key representation.
///
- /// One or more objects.
- /// The for fluent-style method-chaining.
- IEventPublisher Publish(params EventData[] events);
+ /// The values.
+ /// The formatted key.
+ string? FormatKey(IEnumerable
/// The .
/// The optional action.
- public WebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null)
+ /// The optional asynchronous function.
+ public WebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null, Func? beforeRequestAsync = null)
{
HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
BeforeRequest = beforeRequest;
+ BeforeRequestAsync = beforeRequestAsync;
}
///
@@ -30,5 +33,10 @@ public WebApiAgentArgs(HttpClient httpClient, Action? before
/// Gets the to invoke before the Http Request is invoked.
///
public Action? BeforeRequest { get; }
+
+ ///
+ /// Gets the to invoke before the Http Request is invoked (asynchronously).
+ ///
+ public Func? BeforeRequestAsync { get; }
}
}
\ No newline at end of file
diff --git a/src/Beef.Core/WebApi/WebApiAgentBase.cs b/src/Beef.Core/WebApi/WebApiAgentBase.cs
index df9daf153..3e7274a1c 100644
--- a/src/Beef.Core/WebApi/WebApiAgentBase.cs
+++ b/src/Beef.Core/WebApi/WebApiAgentBase.cs
@@ -46,7 +46,7 @@ public static void SetAcceptApplicationJson(HttpClient httpClient)
///
/// Creates the and invokes the .
///
- private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, StringContent? content = null, WebApiRequestOptions? requestOptions = null)
+ private async Task CreateRequestMessageAsync(HttpMethod method, Uri uri, StringContent? content = null, WebApiRequestOptions? requestOptions = null)
{
var req = new HttpRequestMessage(method, uri);
if (content != null)
@@ -58,6 +58,7 @@ private HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, Stri
ApplyWebApiOptions(req, requestOptions);
Args.BeforeRequest?.Invoke(req);
+ await (Args.BeforeRequestAsync?.Invoke(req) ?? Task.CompletedTask).ConfigureAwait(false);
return req;
}
@@ -95,7 +96,7 @@ public async Task GetAsync(string urlSuffix, WebApiRequestOpt
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Get, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Get, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -118,7 +119,7 @@ public async Task> GetAsync(string urlSuffix
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Get, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Get, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -187,7 +188,7 @@ public async Task PutAsync(string urlSuffix, object value, We
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, value, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -216,7 +217,7 @@ public async Task> PutAsync(string urlSuffix
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, value, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -238,7 +239,7 @@ public async Task PutAsync(string urlSuffix, WebApiRequestOpt
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -261,7 +262,7 @@ public async Task> PutAsync(string urlSuffix
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Put, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -289,7 +290,7 @@ public async Task PostAsync(string urlSuffix, object value, W
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, value, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -318,7 +319,7 @@ public async Task> PostAsync(string urlSuffi
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, value, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -341,7 +342,7 @@ public async Task PostAsync(string urlSuffix, WebApiRequestOp
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -365,7 +366,7 @@ public async Task> PostAsync(string urlSuffi
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
var value = args?.Where(x => x.ArgType == WebApiArgType.FromBody).SingleOrDefault()?.GetValue();
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Post, uri, CreateJsonContentFromValue(value), requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -386,7 +387,7 @@ public async Task DeleteAsync(string urlSuffix, WebApiRequest
var uri = CreateFullUri(urlSuffix, args, requestOptions);
return await WebApiAgentInvoker.Current.InvokeAsync(this, async () =>
{
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(HttpMethod.Delete, uri, requestOptions: requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(HttpMethod.Delete, uri, requestOptions: requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, null!, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -420,7 +421,7 @@ public async Task PatchAsync(string urlSuffix, WebApiPatchOpt
{
var content = new StringContent(json.ToString());
content.Headers.ContentType = MediaTypeHeaderValue.Parse(patchOption == WebApiPatchOption.JsonPatch ? "application/json-patch+json" : "application/merge-patch+json");
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(new HttpMethod("PATCH"), uri, content, requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(new HttpMethod("PATCH"), uri, content, requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return VerifyResult(result);
}, json, memberName, filePath, lineNumber).ConfigureAwait(false);
@@ -452,7 +453,7 @@ public async Task> PatchAsync(string urlSuff
{
var content = new StringContent(json.ToString());
content.Headers.ContentType = MediaTypeHeaderValue.Parse(patchOption == WebApiPatchOption.JsonPatch ? "application/json-patch+json" : "application/merge-patch+json");
- var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(CreateRequestMessage(new HttpMethod("PATCH"), uri, content, requestOptions)).ConfigureAwait(false));
+ var result = new WebApiAgentResult(await Args.HttpClient.SendAsync(await CreateRequestMessageAsync(new HttpMethod("PATCH"), uri, content, requestOptions).ConfigureAwait(false)).ConfigureAwait(false));
result.Content = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
return new WebApiAgentResult(VerifyResult(result));
}, json, memberName, filePath, lineNumber).ConfigureAwait(false);
diff --git a/src/Beef.Data.Database.Cdc/CHANGELOG.md b/src/Beef.Data.Database.Cdc/CHANGELOG.md
index 36696d8df..dd02874f5 100644
--- a/src/Beef.Data.Database.Cdc/CHANGELOG.md
+++ b/src/Beef.Data.Database.Cdc/CHANGELOG.md
@@ -4,6 +4,7 @@ Represents the **NuGet** versions.
## v4.1.5
- *Enhancement:* Added standardized identifier mapping from local (internal) to global (external) where required.
+- *Enhancement:* `CdcBackgroundService` renamed to `CdcHostedService`.
## v4.1.4
- *Enhancement:* Added `ILogicallyDeleted.ClearWhereDeleted()` to clear all properties that should not have a value where logically deleted; as the data is technically considered as non-existing.
diff --git a/src/Beef.Data.Database.Cdc/CdcDataOrchestrator.cs b/src/Beef.Data.Database.Cdc/CdcDataOrchestrator.cs
index f6aca65c2..cf81acb29 100644
--- a/src/Beef.Data.Database.Cdc/CdcDataOrchestrator.cs
+++ b/src/Beef.Data.Database.Cdc/CdcDataOrchestrator.cs
@@ -6,8 +6,8 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -124,20 +124,30 @@ public CdcDataOrchestrator(IDatabase db, string executeStoredProcedureName, stri
protected virtual string ServiceName => _name ??= GetType().Name;
///
- /// Gets the .
+ /// Gets the (to be further formatted as per ).
///
protected abstract string EventSubject { get; }
///
- /// Gets the format.
+ /// Gets the .
///
protected virtual EventSubjectFormat EventSubjectFormat { get; } = EventSubjectFormat.NameAndKey;
///
- /// Gets the .
+ /// Gets the .
///
protected virtual EventActionFormat EventActionFormat { get; } = EventActionFormat.None;
+ ///
+ /// Gets the .
+ ///
+ protected virtual Uri? EventSource { get; }
+
+ ///
+ /// Gets the .
+ ///
+ protected virtual EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.NameAndKey;
+
///
/// Gets the list of property names that should be excluded from the serialized JSON generation.
///
@@ -217,11 +227,14 @@ public async Task result;
- Logger.LogInformation($"{ServiceName} Query for next (new) Change Data Capture outbox (MaxQuerySize = {MaxQuerySize}, ContinueWithDataLoss = {ContinueWithDataLoss}).");
+ Logger.LogTrace($"{ServiceName} Query for next (new) Change Data Capture outbox. [MaxQuerySize={MaxQuerySize}, ContinueWithDataLoss={ContinueWithDataLoss}]");
+
+ var sw = Stopwatch.StartNew();
try
{
result = await GetOutboxEntityDataAsync().ConfigureAwait(false);
+ sw.Stop();
}
catch (Exception ex)
{
@@ -243,11 +256,11 @@ public async Task 0)
{
+ sw = Stopwatch.StartNew();
result.Events = (await CreateEventsAsync(coll2, cancellationToken.Value).ConfigureAwait(false)).ToArray();
await EventPublisher.Publish(result.Events).SendAsync().ConfigureAwait(false);
- Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': {result.Events.Length} event(s) were published/sent successfully.");
+ sw.Stop();
+ Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': {result.Events.Length} event(s) were published/sent successfully. [{sw.ElapsedMilliseconds}ms]");
}
else
- Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': No event(s) were published; no unique tracking hash found.");
+ {
+ sw.Stop();
+ Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': No event(s) were published; no unique tracking hash found. [{sw.ElapsedMilliseconds}ms]");
+ }
// Complete the outbox (ignore any further 'cancel' as event(s) have been published and we *must* complete to minimise chance of sending more than once).
+ sw = Stopwatch.StartNew();
await CompleteAsync(result.Outbox.Id, tracking).ConfigureAwait(false);
- Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': Marked as Completed.");
+ sw.Stop();
+ Logger.LogInformation($"{ServiceName} Outbox '{result.Outbox.Id}': Marked as Completed. [{sw.ElapsedMilliseconds}ms]");
return result;
}
@@ -405,65 +430,74 @@ protected async Task AssignIdentityMappingAsync(TCdcEntityWrapperColl coll)
///
/// The .
/// The value.
- /// The name (will be formated as per ).
- /// The to infer the .
+ /// The name (will be formated as per ).
+ /// The to infer the .
/// The .
protected EventData CreateValueEvent(T value, string subjectName, OperationType operationType) where T : class
- => EventData.CreateValueEvent(value, CreateValueFormattedSubject(value, subjectName), EventActionFormatter.Format(operationType, EventActionFormat));
+ {
+ var gid = value as IGlobalIdentifier;
+ var ed = EventSource == null
+ ? EventData.CreateValueEvent(value, CreateValueFormattedSubject(value, subjectName), EventActionFormatter.Format(operationType, EventActionFormat))
+ : EventData.CreateValueEvent(value, CreateFormattedUri(EventSource!, EventSourceFormat == EventSourceFormat.NameAndGlobalId && gid?.GlobalId != null ? gid.GlobalId : EventPublisher.FormatKey(value)),
+ CreateValueFormattedSubject(value, subjectName), EventActionFormatter.Format(operationType, EventActionFormat));
+
+ if (gid != null)
+ ed.Key = gid.GlobalId;
+
+ return ed;
+ }
///
/// Creates an with the specified .
///
- /// The name (will be formated as per ).
- /// The to infer the .
+ /// The name (will be formated as per ).
+ /// The to infer the .
/// The event key.
/// The .
- protected EventData CreateEvent(string subjectName, OperationType operationType, params IComparable?[] key)
- => EventData.CreateEvent(CreateFormattedSubject(subjectName, key), EventActionFormatter.Format(operationType, EventActionFormat), key);
+ protected EventData CreateEvent(string subjectName, OperationType operationType, params IComparable?[] key) => EventSource == null
+ ? EventData.CreateEvent(CreateFormattedSubject(subjectName, key), EventActionFormatter.Format(operationType, EventActionFormat), key)
+ : EventData.CreateEvent(CreateFormattedUri(EventSource!, EventPublisher.FormatKey(key)), CreateFormattedSubject(subjectName, key), EventActionFormatter.Format(operationType, EventActionFormat), key);
///
/// Creates a fully qualified event subject as per .
///
/// The .
/// The value.
- /// The name.
+ /// The name.
/// The fully qualified subject.
/// must implement at least one of the following: , , or .
- protected string CreateValueFormattedSubject(T value, string subjectName) where T : class => EventSubjectFormat == EventSubjectFormat.NameOnly ? subjectName : subjectName + "." + CreateValueKey(value);
+ protected string CreateValueFormattedSubject(T value, string subjectName) where T : class => EventSubjectFormat == EventSubjectFormat.NameOnly ? subjectName : subjectName + "." + CreateValueFormattedSubjectKey(value);
///
- /// Creates the key (as a ) for the .
+ /// Creates the event subject key (as a ) for the .
///
/// The .
/// The value.
/// The key for the .
- protected static string CreateValueKey(T value) where T : class => value is IGlobalIdentifier gi && gi.GlobalId != null ? gi.GlobalId : value.CreateFormattedKey();
+ protected string CreateValueFormattedSubjectKey(T value) where T : class => value is IGlobalIdentifier gi && gi.GlobalId != null ? gi.GlobalId : EventPublisher.FormatKey(value)!;
+
+ ///
+ /// Creates the formatted tracking key (as a ) for the .
+ ///
+ /// The .
+ /// The value.
+ /// The formatted tracking key.
+ protected string CreateTrackingKey(T value) where T : class => value is IGlobalIdentifier gi && gi.GlobalId != null ? gi.GlobalId : value.CreateIdentifierMappingKey()!;
///
/// Creates a fully qualified event subject by appending the to the .
///
- /// The prefix.
+ /// The prefix.
/// The event key.
/// The fully qualified subject.
- protected string CreateFormattedSubject(string subjectName, params IComparable?[] key)
- {
- if (EventSubjectFormat == EventSubjectFormat.NameOnly)
- return subjectName;
-
- var sb = new StringBuilder(subjectName + ".");
- if (key == null || key.Length == 0)
- throw new ArgumentException("There must be at least a single key value specified.", nameof(key));
+ private string CreateFormattedSubject(string subjectName, params IComparable?[] key)
+ => (EventSubjectFormat == EventSubjectFormat.NameOnly) ? subjectName : $"{subjectName}{EventPublisher.PathSeparator}{EventPublisher.FormatKey(key)}";
- for (int i = 0; i < key.Length; i++)
- {
- if (i > 0)
- sb.Append(",");
-
- sb.Append(key[i]);
- }
-
- return sb.ToString();
- }
+ ///
+ /// Creates new, or returns, the Uri based on the EventSourceFormat.
+ ///
+ private Uri CreateFormattedUri(Uri uri, string? path)
+ => (EventSourceFormat == EventSourceFormat.NameOnly || string.IsNullOrEmpty(path)) ? uri : new Uri($"{uri.ToString().TrimEnd('/')}/{path}", uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative);
///
/// Creates none or more events from the entity collection..
diff --git a/src/Beef.Data.Database.Cdc/CdcExtensions.cs b/src/Beef.Data.Database.Cdc/CdcExtensions.cs
index a73cf928a..a7dc99fe4 100644
--- a/src/Beef.Data.Database.Cdc/CdcExtensions.cs
+++ b/src/Beef.Data.Database.Cdc/CdcExtensions.cs
@@ -18,17 +18,17 @@ public static class CdcExtensions
///
public const string ServicesName = "Services";
- private const string _suffix = "BackgroundService";
+ private const string _suffix = "HostedService"; // Standard (code-generated) naming convention.
///
/// Adds a as an . Before adding checks whether the has been specified within the
/// comma-separated list of Services defined in the .
///
- /// The / .
+ /// The / .
/// The .
/// The .
/// The for fluent-style method-chaining.
- public static IServiceCollection AddCdcHostedService(this IServiceCollection services, IConfiguration config) where TCdcService : CdcBackgroundService, IHostedService
+ public static IServiceCollection AddCdcHostedService(this IServiceCollection services, IConfiguration config) where TCdcService : CdcHostedService, IHostedService
{
var svcs = (config ?? throw new ArgumentNullException(nameof(config))).GetValue(ServicesName);
if (svcs == null)
@@ -45,11 +45,11 @@ public static IServiceCollection AddCdcHostedService(this IServiceC
}
///
- /// Creates the formatted key (as a ) for the .
+ /// Creates the formatted identifier mapping key (as a ) for the .
///
/// The value.
- /// The key for the .
- public static string CreateFormattedKey(this object value)
+ /// The identifier mapping key for the .
+ public static string CreateIdentifierMappingKey(this object value)
{
var sb = new StringBuilder();
switch (value ?? throw new ArgumentNullException(nameof(value)))
diff --git a/src/Beef.Data.Database.Cdc/CdcBackgroundService.cs b/src/Beef.Data.Database.Cdc/CdcHostedService.cs
similarity index 90%
rename from src/Beef.Data.Database.Cdc/CdcBackgroundService.cs
rename to src/Beef.Data.Database.Cdc/CdcHostedService.cs
index fb579f133..8ecd0dd34 100644
--- a/src/Beef.Data.Database.Cdc/CdcBackgroundService.cs
+++ b/src/Beef.Data.Database.Cdc/CdcHostedService.cs
@@ -11,9 +11,9 @@
namespace Beef.Data.Database.Cdc
{
///
- /// Provides the base capabilities for the Change Data Capture (CDC) background services.
+ /// Provides the base (non-generics) capabilities for the Change Data Capture (CDC) services.
///
- public abstract class CdcBackgroundService : IDisposable
+ public abstract class CdcHostedService : IDisposable
{
private string? _name;
private int? _intervalSeconds;
@@ -41,12 +41,12 @@ public abstract class CdcBackgroundService : IDisposable
public static int DefaultIntervalSeconds { get; set; } = 60;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public CdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger)
+ public CdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
Config = config ?? throw new ArgumentNullException(nameof(config));
@@ -122,16 +122,16 @@ public void Dispose()
}
///
- /// Releases the unmanaged resources used by the and optionally releases the managed resources.
+ /// Releases the unmanaged resources used by the and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing) { }
}
///
- /// Represents the base class for Change Data Capture (CDC) background services.
+ /// Represents the base class for Change Data Capture (CDC) services.
///
- public abstract class CdcBackgroundService : CdcBackgroundService, IHostedService where TCdcDataOrchestrator : ICdcDataOrchestrator
+ public abstract class CdcHostedService : CdcHostedService, IHostedService where TCdcDataOrchestrator : ICdcDataOrchestrator
{
private static readonly Random _random = new Random();
@@ -141,12 +141,12 @@ public abstract class CdcBackgroundService : CdcBackground
private bool _disposed;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public CdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
+ public CdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger logger) : base(serviceProvider, config, logger) { }
///
/// Triggered when the application host is ready to start the service.
@@ -156,7 +156,7 @@ public CdcBackgroundService(IServiceProvider serviceProvider, IConfiguration con
/// The underlying timer start is randomized between zero and one thousand milliseconds; this will minimize multiple services within the host all starting at once.
Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
- Logger.LogInformation($"{ServiceName} service started.");
+ Logger.LogInformation($"{ServiceName} service started. Execution interval {IntervalTimespan}.");
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_timer = new Timer(Execute, null, TimeSpan.FromMilliseconds(_random.Next(0, 1000)), IntervalTimespan);
return Task.CompletedTask;
@@ -169,12 +169,12 @@ private void Execute(object? state)
{
// Stop the timer as no more work should be initiated until after complete.
_timer!.Change(Timeout.Infinite, Timeout.Infinite);
- Logger.LogInformation($"{ServiceName} execution triggered by timer.");
+ Logger.LogTrace($"{ServiceName} execution triggered by timer.");
_executeTask = Task.Run(async () => await ScopedExecuteAsync(_cts!.Token).ConfigureAwait(false));
_executeTask.Wait();
- Logger.LogInformation($"{ServiceName} execution completed. Retry in {IntervalTimespan}.");
+ Logger.LogTrace($"{ServiceName} execution completed. Retry in {IntervalTimespan}.");
_timer?.Change(IntervalTimespan, IntervalTimespan);
}
@@ -213,7 +213,7 @@ private async Task ScopedExecuteAsync(CancellationToken cancellationToken)
/// The . Defaults to .
/// The .
/// This method is provided where a single invocation is required versus executing as a background service using the capabilties. Note that the
- /// is not used when running directly via this method.
+ /// is not used when running directly via this method.
public async Task RunAsync(CancellationToken? cancellationToken = null)
{
// Set up the execution context.
@@ -240,7 +240,7 @@ private async Task ExecuteAsync(CancellationToken can
if (cwdl.HasValue)
cdo.ContinueWithDataLoss = cwdl.Value;
- // Keep executing until unsucessful or reached end of CDC data.
+ // Keep executing until unsuccessful or reached end of CDC data.
while (true)
{
var result = await cdo.ExecuteAsync(cancellationToken).ConfigureAwait(false);
@@ -276,7 +276,7 @@ async Task IHostedService.StopAsync(CancellationToken cancellationToken)
}
///
- /// Releases the unmanaged resources used by the and optionally releases the managed resources.
+ /// Releases the unmanaged resources used by the and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
diff --git a/src/Beef.Core/Events/EventActionFormat.cs b/src/Beef.Data.Database.Cdc/EventActionFormat.cs
similarity index 68%
rename from src/Beef.Core/Events/EventActionFormat.cs
rename to src/Beef.Data.Database.Cdc/EventActionFormat.cs
index 7bce319d7..9ee9adc2a 100644
--- a/src/Beef.Core/Events/EventActionFormat.cs
+++ b/src/Beef.Data.Database.Cdc/EventActionFormat.cs
@@ -1,35 +1,25 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-namespace Beef.Events
+namespace Beef.Data.Database.Cdc
{
///
- /// Defines the event action format.
+ /// Defines the event subject format.
///
public enum EventActionFormat
{
///
- /// No formatting; as-is (default).
+ /// No formatting, leave action as-is.
///
None,
///
- /// Format as upper case.
+ /// The action as past-tense.
///
- UpperCase,
-
- ///
- /// Format as past tense.
- ///
- PastTense,
-
- ///
- /// Format as past tense upper case.
- ///
- PastTenseUpperCase
+ PastTense
}
///
- /// Provides the event action formatting capability.
+ /// Provides the formatting capability.
///
public static class EventActionFormatter
{
@@ -41,9 +31,7 @@ public static class EventActionFormatter
/// The formatted action.
public static string Format(string action, EventActionFormat? format) => format switch
{
- EventActionFormat.UpperCase => Check.NotEmpty(action, nameof(action)).ToUpperInvariant()!,
EventActionFormat.PastTense => StringConversion.ToPastTense(Check.NotEmpty(action, nameof(action)))!,
- EventActionFormat.PastTenseUpperCase => StringConversion.ToPastTense(Check.NotEmpty(action, nameof(action)))!.ToUpperInvariant(),
_ => Check.NotEmpty(action, nameof(action))
};
diff --git a/src/Beef.Data.Database.Cdc/EventSourceFormat.cs b/src/Beef.Data.Database.Cdc/EventSourceFormat.cs
new file mode 100644
index 000000000..6bcddc8ce
--- /dev/null
+++ b/src/Beef.Data.Database.Cdc/EventSourceFormat.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+namespace Beef.Data.Database.Cdc
+{
+ ///
+ /// Defines the event source format.
+ ///
+ public enum EventSourceFormat
+ {
+ ///
+ /// The source name appended with the .
+ ///
+ NameAndKey,
+
+ ///
+ /// The source name appended with the where exists; otherwise, will fall back to .
+ ///
+ NameAndGlobalId,
+
+ ///
+ /// The source name only.
+ ///
+ NameOnly
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Data.Database.Cdc/README.md b/src/Beef.Data.Database.Cdc/README.md
index 79304650f..3122aa5aa 100644
--- a/src/Beef.Data.Database.Cdc/README.md
+++ b/src/Beef.Data.Database.Cdc/README.md
@@ -16,7 +16,7 @@ This [article](https://www.mssqltips.com/sqlservertip/5212/sql-server-temporal-t
## Approach
-The CDC approach taken here is to consolidate the tracking of individual tables (one or more) into a central entity to simplify the publishing to an event stream (or equivalent). The advantage of this is where a change occurs to any of the rows related to an entity, even where multiples rows are updated, this will only result in a single event. This makes it easier (more logical) for downstream subscribers to consume.
+The CDC approach taken here is to consolidate the tracking of individual tables (one or more) into a central _entity_ to simplify the publishing to an event stream (or equivalent). The advantage of this is where a change occurs to any of the rows related to an entity, even where multiples rows are updated, this will only result in a single event. This makes it easier (more logical) for downstream subscribers to consume.
This is achieved by defining (configuring) the entity, being the primary (parent) table, and its related secondary (child) tables. For example, a Sales Order, may be made up multiple tables - when any of these change then a single _SalesOrder_ event should occur. These relationships are also defined with a cardinality of either `OneToMany` or `OneToOne`.
@@ -28,17 +28,19 @@ SalesOrder // Parent
The CDC capability is used specifically as a trigger for change (being `Create`, `Update` or `Delete`). The resulting data that is published is the latest, not a snapshot in time (CDC captured). The reason for this is two-fold, a) given how the CDC data is retrieved there is no guarantee that the interim data represents a final intended state suitable for publishing; and b) this process should be running near real-time so getting the latest version will produce the current committed version as at that time.
-To further guarantee only a single event for a specific version is published the resulting entity is JSON serialized and hashed; this value is checked (and saved) against the prior version to ensure a publish contains data that is actionable. This will minimize redundant publishing, whilst also making the underlying processing more efficient.
+To further guarantee only a single event for a specific version is published the resulting _entity_ is JSON serialized and hashed; this value is checked (and saved) against the prior version to ensure a publish contains data that is actionable. This will minimize redundant publishing, whilst also making the underlying processing more efficient.
## Set up
-The first activity is to enable CDC on the database and then enable on each of the tables; using the SQL Server system (native) stored procedures:
-- [`sys.sp_cdc_enable_db`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sys-sp-cdc-enable-db-transact-sql) - enables the database.
-- [`sys.sp_cdc_enable_table`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sys-sp-cdc-enable-table-transact-sql) - enables the table. Please note that `@supports_net_changes` need not be selected as they are not used.
+The first activity is to enable CDC on the database, and then enable on each of the required tables. This is performed using the SQL Server system (native) stored procedures:
+- [`sys.sp_cdc_enable_db`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sys-sp-cdc-enable-db-transact-sql) - enables for the database.
+- [`sys.sp_cdc_enable_table`](https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sys-sp-cdc-enable-table-transact-sql) - enables for the specified table. The `@supports_net_changes` need not be selected as the netted changes are not used.
-An example is as follows.
+_Note:_ for `sys.sp_cdc_enable_table` where the `@captured_column_list` is not specified all columns (as at execution time) will be included in the capture; this will need to be re-executed in the future as schema changes are made. The _Beef_ CDC processing does not rely on the captured columns beyond the primary and related join columns. Usage of a [`rowversion`](https://docs.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql) column is an effective means to trigger CDC over time without having to capture all columns as it is guaranteed to change on every row update. This could also be a means to minimize the volume of data being captured;
+
+An example of the SQL script is as follows.
``` sql
-- Enable for the database.
@@ -48,7 +50,7 @@ BEGIN
EXEC sys.sp_cdc_enable_db
END
--- Enable for the seleted table.
+-- Enable for the selected table.
IF (SELECT TOP 1 is_tracked_by_cdc FROM sys.tables WHERE [OBJECT_ID] = OBJECT_ID(N'SchemaName.TableName')) = 0
BEGIN
EXEC sys.sp_cdc_enable_table
@@ -59,7 +61,7 @@ BEGIN
END
```
-Where using [Migration](./../../tools/Beef.Database.Core/README.md) Scripts [code-generation](#Code-generation) can be leveraged to accelerate. The following command line executions will create the migration scripts that contain the contents described above.
+Where using [DbUp Migration](./../../tools/Beef.Database.Core/README.md) Scripts [code-generation](#Code-generation) can be leveraged to accelerate. The following command line executions will create the migration scripts that largely contain the contents described above.
```
-- Create sys.sp_cdc_enable_db script.
@@ -75,7 +77,7 @@ dotnet run scriptnew cdc SchemaName TableName
The remainder of the database objects required to support CDC event publishing is generated using [database code-generation](../../tools/Beef.Database.Core/README.md); as is the corresponding .NET code to process. This is achieved using the CDC-related configuration.
-The code-generation is applicable whether using a Data-tier application (DAC) or DbUp to manage the database.
+The code-generation is applicable whether using a [Data-tier Application](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications) (DAC) or DbUp to manage the database.
@@ -96,78 +98,56 @@ Configuration details for each of the above are as follows:
- CdcJoin - [YAML/JSON](../../docs/Database-CdcJoin-Config.md) or [XML](../../docs/Database-CdcJoin-Config-Xml.md)
- CdcJoinOn - [YAML/JSON](../../docs/Database-CdcJoinOn-Config.md) or [XML](../../docs/Database-CdcJoinOn-Config-Xml.md)
-The following represents an [example](../../samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml) of the XML configuration.
-
-``` xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+The following represents an example of the YAML configuration. This scenario is based on a parent `dbo.Person` table, and corresponding `dbo.PersonAddress` and `dbo.PersonPhone` (one-to-many) related tables. Additionally, see also the Demo testing [example](../../samples/Demo/Beef.Demo.Database/Beef.Demo.Database.xml).
+
+``` yaml
+schema: dbo
+cdcSchema: XxCdc
+cdcIdentifierMapping: true
+cdcExcludeColumnsFromETag: [ CreateUser, CreateDate, UpdateUser, UpdateDate, VersionNumber ]
+autoDotNetRename: SnakeKebabToPascalCase
+pathDatabaseSchema: Xx.LegacyDb.Database
+pathCdc: Xx.LegacyDb.CdcService
+namespaceCdc: Xx.LegacyDb.CdcService
+eventSubjectFormat: NameOnly
+eventSubjectRoot: Legacy
+eventActionFormat: PastTense
+eventSourceRoot: /legacy_db
+eventSourceKind: Relative
+hasBeefDbo: false
+cdc:
+- { name: Person, identifierMapping: true, excludeColumns: [ SAPId, VersionNumber ],
+ joins: [
+ { name: PersonAddress, propertyName: Addresses, excludeColumns: [ VersionNumber ], on: [ { name: PersonId } ] },
+ { name: PersonPhone, propertyName: PhoneNumbers, excludeColumns: [ VersionNumber ], on: [ { name: PersonId } ] },
+ { name: AddressType, type: Left, joinTo: PersonAddress, includeColumns: [ AddressTypeCode ], on: [ { name: AddressTypeId } ] },
+ { name: PhoneType, type: Left, joinTo: PersonPhone, includeColumns: [ PhoneTypeName ], on: [ { name: PhoneTypeId } ] }
+ ]
+ }
```
### Database code generation
-The underlying [`Program.cs`](./../../samples/Demo/Beef.Demo.Database/Program.cs) should be updated to override the `DatabaseScript` depending on whether using a Data-tier application (DAC) or DbUp to manage the database. Where using DbUp the required database tables will need to be added as Migration scripts explicitly, versus being generated automatically.
+The underlying [`Program.cs`](./../../samples/Demo/Beef.Demo.Database/Program.cs) should be updated to override the `DatabaseScript` depending on whether using a Data-tier Application (DAC) or DbUp to manage the database. Where using DbUp the required database tables will need to be added as Migration scripts explicitly, versus being generated automatically.
``` csharp
// Using DbUp; use: DatabaseWithCdc.xml
-return DatabaseConsoleWrapper
- .Create("Data Source=.;Initial Catalog=Beef.Demo;Integrated security=True", "Beef", "Demo")
- .DatabaseScript("DatabaseWithCdc.xml")
+// Uses the connection string defined in Environment Variable: Xx_LegacyDb_ConnectionString
+// To run execute command line: dotnet run database
+static Task Main(string[] args) => CodeGenConsoleWrapper
+ .Create("Xx", "LegacyDb")
+ .Supports(entity: false, database: true)
+ .DatabaseScript("DatabaseWithCdcDacpac.xml")
.RunAsync(args);
// Using DACPAC; use: DatabaseWithCdcDacpac.xml
-return DatabaseConsoleWrapper
- .Create("Data Source=.;Initial Catalog=Beef.Demo;Integrated security=True", "Beef", "Demo")
+// Uses the connection string defined in Environment Variable: Xx_LegacyDb_ConnectionString
+// To run execute command line: dotnet run database
+static Task Main(string[] args) => CodeGenConsoleWrapper
+ .Create("Xx", "LegacyDb")
+ .Supports(entity: false, database: true)
.DatabaseScript("DatabaseWithCdcDacpac.xml")
.RunAsync(args);
```
@@ -186,24 +166,27 @@ The following database artefacts are generated.
Type | Name | Description
-|-|-
`Table` | `CdcTracking.sql` | Represents the related _Entity Hash_ tracking table used to identify whether a version of a specific entity has been previously (successfully) processed. See [example](../../samples/Demo/Beef.Demo.Database/Migrations/20210111-163724-create-democdc-cdctracking.sql).
+`Table` | `CdcIdentifierMapping.sql` | Represents the related _Identifier Mapping_ table that manages the relationship between global identifiers and existing database identifiers. This is optional; where identifier mapping is desired this is determined by using the global `CdcIdentifierMapping` code-generation configuration setting. See [example](../../samples/Demo/Beef.Demo.Database/Migrations/20210322-233437-create-democdc-cdcidentifiermapping.sql).
`Table` | `XxxOutbox.sql` | Represents the _Entity_ outbox table used to track the log sequence number (LSN) for the primary and secondary tables. This acts as a pointer of where the processing is at in relation to each table to aid both reprocessing, and to determine where to begin processing of next outbox set. An outbox set is essentially just a batch of one or more entities for processing. See [example](../../samples/Demo/Beef.Demo.Database/Migrations/20210111-163747-create-democdc-postsoutbox.sql).
`Type` | `udtTrackingList.sql` | Represents the user-defined type / table-valued parameter required to pass a list of key/hash values from .NET code to a SQL Stored Procedure. See [example](../../samples/Demo/Beef.Demo.Database/Schema/DemoCdc/Types/User-Defined%20Table%20Types/Generated/UdtCdcTrackingList.sql).
+`Type` | `udtIdentifierMapping.sql` | Represents the user-defined type / table-valued parameter required to pass a list of keys and global identifiers for assignment and update. This is optional; where identifier mapping is desired; this is determined by using the global `CdcIdentifierMapping` code-generation configuration setting. See [example](../../samples/Demo/Beef.Demo.Database/Schema/DemoCdc/Types/User-Defined%20Table%20Types/Generated/udtCdcIdentifierMappingList.sql).
`Stored Procedure` | `spExecuteXxxCdcOutbox.sql` | Represents the **key** CDC-related execution logic. This stored procedure is responsible for getting the next outbox set for an _Entity_, or retrying an existing outbox set. See [example](../../samples/Demo/Beef.Demo.Database/Schema/DemoCdc/Stored%20Procedures/Generated/spExecuteContactCdcOutbox.sql).
`Stored Procedure` | `spCompleteXxxCdcOutbox.sql` | Represents the **key** CDC-related completion logic. This stored procedure is responsible for completing an existing outbox set. See [example](../../samples/Demo/Beef.Demo.Database/Schema/DemoCdc/Stored%20Procedures/Generated/spCompleteContactCdcOutbox.sql).
+`Stored Procedure` | `spCreateCdcIdentifierMapping.sql` | Represents the stored procedure that assigns and returns the selected keys and their respectivce global identifiers.
_Tip:_ If any of the generated files are not automatically added to the Visual Studio Project structure, the _Show All Files_ in the _Solution Explorer_ can be used to view, and then individually added using _Include In Project_.
As [stated](#Database-code-generation) earlier, where using DbUp the [Migration](./../../tools/Beef.Database.Core/README.md) Scripts [code-generation](#Code-generation) must be explicitly executed. The following represents the command line executions required.
```
--- Create the CdcTracking.sql
+-- Create the CDC Trcking table - CdcTracking.sql
dotnet run codegen --script DatabaseCdcTracking.xml
--- Create (each) `XxxOutbox.sql` by specifying the unique CdcName as configured.
+-- Create (each) of the CDC Outbox tables - `XxxOutbox.sql` by specifying the unique CdcName as configured.
dotnet run codegen --script DatabaseCdcOutbox.xml --param CdcName=Contact
dotnet run codegen --script DatabaseCdcOutbox.xml --param CdcName=Posts
--- Create the CdcIdentityMapping.sql
+-- Create the CDC Identifier Mapping table - CdcIdentifierMapping.sql
dotnet run codegen --script DatabaseCdcIdentityMapping.xml
```
@@ -218,6 +201,6 @@ Type | Name | Description
`Entity` | `XxxCdc.cs` | Represents the database tables as .NET entities (classes), where the columns map to properties, and are marked up for JSON serialization. The classes also implement [`IUniqueKey`](../Beef.Core/Entities/IUniqueKey.cs) and [`IETag`](../Beef.Core/Entities/IETag.cs) where applicable so that other _Beef_ related capabilities can be leveraged. See [example](../../samples/Demo/Beef.Demo.Cdc/Entities/Generated/ContactCdc.cs).
`Data` | `XxxCdcData.cs` | Represents the key data and event orchestration for an _Entity_. Inherits the base capabilities from [`CdcDataOrchestrator`](../Beef.Data.Database.Cdc/CdcDataOrchestrator.cs). This is largely responsible for implementing the data reader into the resulting _Entity_, creating the corresponding [`EventData`](../Beef.Core/Events/EventData.cs) (can be overridden), and invoking the [`IEventPublisher`](../Beef.Core/Events/IEventPublisher.cs) to publish (this can be implemented to publish to any streaming/messaging capability as required). See [example](../../samples/Demo/Beef.Demo.Cdc/Data/Generated/ContactCdcData.cs).
`Data` | `ServiceCollectionsExtension.cs` | Represents the addition of the scoped services ([IServiceCollection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection)) for the aforementioned `XxxCdcData` classes. See [example](../../samples/Demo/Beef.Demo.Cdc/Data/Generated/ServiceCollectionsExtension.cs).
-`Service` | `XxxCdcBackgroundService.cs` | Represents the .NET background service for CDC-related processing. Inherits the base capabailities from [`CdcBackgroundService`](../Beef.Data.Database.Cdc/CdcBackgroundService.cs) which is an [`IHostedService`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) implementation with a timer-based processing trigger. See [example](../../samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcBackgroundService.cs).
+`Service` | `XxxCdcBackgroundService.cs` | Represents the .NET background service for CDC-related processing. Inherits the base capabailities from [`CdcHostedService`](../Beef.Data.Database.Cdc/CdcHostedService.cs) which is an [`IHostedService`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) implementation with a timer-based processing trigger. See [example](../../samples/Demo/Beef.Demo.Cdc/Services/Generated/ContactCdcHostedService.cs).
diff --git a/src/Beef.Events.EventHubs/AzureEventHubsEventConverter.cs b/src/Beef.Events.EventHubs/AzureEventHubsEventConverter.cs
new file mode 100644
index 000000000..a6005fca6
--- /dev/null
+++ b/src/Beef.Events.EventHubs/AzureEventHubsEventConverter.cs
@@ -0,0 +1,178 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Threading.Tasks;
+using AzureEventHubs = Azure.Messaging.EventHubs;
+
+namespace Beef.Events.EventHubs
+{
+ ///
+ /// Represents an Azure Event Hubs (see ) converter.
+ ///
+ public sealed class AzureEventHubsEventConverter : IEventDataConverter
+ {
+ private readonly IEventDataContentSerializer _contentSerializer;
+
+ ///
+ /// Creates a new instance of using the specified .
+ ///
+ /// The .
+ /// The corresponding .
+ /// A new instance of
+ internal static EventData CreateValueEventData(Type valueType, EventMetadata? metadata) => (EventData)Activator.CreateInstance(typeof(EventData<>).MakeGenericType(new Type[] { valueType }), new object[] { metadata! });
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The . Defaults to .
+ public AzureEventHubsEventConverter(IEventDataContentSerializer? contentSerializer = null) => _contentSerializer = contentSerializer ?? new NewtonsoftJsonCloudEventSerializer();
+
+ ///
+ /// Indicates whether to write to the for the .
+ ///
+ public bool UseMessagingPropertiesForMetadata { get; set; }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(AzureEventHubs.EventData @event)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(@event.Body.ToArray()).ConfigureAwait(false)) ?? new EventData(null);
+ await MergeMetadataAndFixKeyAsync(ed, @event).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(Type valueType, AzureEventHubs.EventData @event)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(valueType, @event.Body.ToArray()).ConfigureAwait(false)) ?? CreateValueEventData(valueType, await GetMetadataAsync(@event).ConfigureAwait(false));
+ await MergeMetadataAndFixKeyAsync(ed, @event).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Merge in the metadata and fix the key.
+ ///
+ private async Task MergeMetadataAndFixKeyAsync(EventData ed, AzureEventHubs.EventData eh)
+ {
+ if (!UseMessagingPropertiesForMetadata)
+ return;
+
+ var md = await GetMetadataAsync(eh).ConfigureAwait(false);
+ if (md != null)
+ {
+ ed.MergeMetadata(md);
+
+ // Always override the key as it doesn't lose the type like the serialized value does.
+ if (eh.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
+ ed.Key = key;
+ }
+ }
+
+ ///
+ /// Converts an to a .
+ ///
+ /// The .
+ /// The .
+ public async Task ConvertToAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (string.IsNullOrEmpty(@event.Subject))
+ throw new ArgumentException("Subject property is required to be set.", nameof(@event));
+
+ var bytes = await _contentSerializer.SerializeAsync(@event).ConfigureAwait(false);
+ var ehed = new AzureEventHubs.EventData(new BinaryData(bytes));
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ ehed.Properties.Add(EventMetadata.SubjectAttributeName, @event.Subject);
+
+ if (@event.EventId != null)
+ ehed.Properties.Add(EventMetadata.EventIdAttributeName, @event.EventId);
+
+ if (@event.Action != null)
+ ehed.Properties.Add(EventMetadata.ActionAttributeName, @event.Action);
+
+ if (@event.TenantId != null)
+ ehed.Properties.Add(EventMetadata.TenantIdAttributeName, @event.TenantId);
+
+ if (@event.Source != null)
+ ehed.Properties.Add(EventMetadata.SourceAttributeName, @event.Source);
+
+ if (@event.Key != null)
+ ehed.Properties.Add(EventMetadata.KeyPropertyName, @event.Key);
+
+ if (@event.ETag != null)
+ ehed.Properties.Add(EventMetadata.ETagAttributeName, @event.ETag);
+
+ if (@event.Username != null)
+ ehed.Properties.Add(EventMetadata.UsernameAttributeName, @event.Username);
+
+ if (@event.UserId != null)
+ ehed.Properties.Add(EventMetadata.UserIdAttributeName, @event.UserId);
+
+ if (@event.Timestamp != null)
+ ehed.Properties.Add(EventMetadata.TimestampAttributeName, @event.Timestamp);
+
+ if (@event.CorrelationId != null)
+ ehed.Properties.Add(EventMetadata.CorrelationIdAttributeName, @event.CorrelationId);
+
+ if (@event.PartitionKey != null)
+ ehed.Properties.Add(EventMetadata.PartitionKeyAttributeName, @event.PartitionKey);
+ }
+
+ return ehed;
+ }
+
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ public async Task GetMetadataAsync(AzureEventHubs.EventData message)
+ {
+ if (message == null)
+ return null;
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ message.Properties.TryGetValue(EventMetadata.SubjectAttributeName, out var subject);
+ message.Properties.TryGetValue(EventMetadata.ActionAttributeName, out var action);
+ message.Properties.TryGetValue(EventMetadata.CorrelationIdAttributeName, out var correlationId);
+ message.Properties.TryGetValue(EventMetadata.PartitionKeyAttributeName, out var partitionKey);
+ message.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key);
+ message.Properties.TryGetValue(EventMetadata.ETagAttributeName, out var etag);
+ message.Properties.TryGetValue(EventMetadata.UsernameAttributeName, out var username);
+ message.Properties.TryGetValue(EventMetadata.UserIdAttributeName, out var userId);
+
+ return new EventMetadata
+ {
+ EventId = (message.Properties.TryGetValue(EventMetadata.EventIdAttributeName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null,
+ TenantId = (message.Properties.TryGetValue(EventMetadata.TenantIdAttributeName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
+ Subject = (string?)subject,
+ Action = (string?)action,
+ Source = (message.Properties.TryGetValue(EventMetadata.SourceAttributeName, out var src) && src != null && src is Uri) ? (Uri?)src : null,
+ Key = key,
+ ETag = (string)etag,
+ Username = (string?)username,
+ UserId = (string?)userId,
+ Timestamp = (message.Properties.TryGetValue(EventMetadata.TimestampAttributeName, out var time) && time != null && time is DateTime?) ? (DateTime?)time : null,
+ CorrelationId = (string?)correlationId,
+ PartitionKey = (string?)partitionKey
+ };
+ }
+
+ // Try deserializing to get metadata where possible.
+ return await _contentSerializer.DeserializeAsync(message.Body.ToArray()).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events.EventHubs/Beef.Events.EventHubs.csproj b/src/Beef.Events.EventHubs/Beef.Events.EventHubs.csproj
index 0bb741c6b..d5456887f 100644
--- a/src/Beef.Events.EventHubs/Beef.Events.EventHubs.csproj
+++ b/src/Beef.Events.EventHubs/Beef.Events.EventHubs.csproj
@@ -2,7 +2,7 @@
netstandard2.1
- 4.1.2
+ 4.1.3Beef DevelopersAvanadeBusiness Entity Execution Framework (Beef) Event Hubs framework.
diff --git a/src/Beef.Events.EventHubs/CHANGELOG.md b/src/Beef.Events.EventHubs/CHANGELOG.md
index 5909a8ca9..21b5b16ba 100644
--- a/src/Beef.Events.EventHubs/CHANGELOG.md
+++ b/src/Beef.Events.EventHubs/CHANGELOG.md
@@ -2,6 +2,10 @@
Represents the **NuGet** versions.
+## v4.1.3
+- *Enhancement:* Support new `IEventDataContentSerializer` and `IEventDataConverter`.
+- *Enhancement:* Added `AzureEventHubsMessageConverter` and `MicrosoftEventHubsMessageConverter` for their respective, different, SDK versions.
+
## v4.1.2
- *Enhancement:* Leverage the new `Beef.Events.EventMetadata` class that houses the _Beef_ metadata property names.
diff --git a/src/Beef.Events.EventHubs/EventDataMapper.cs b/src/Beef.Events.EventHubs/EventDataMapper.cs
deleted file mode 100644
index 0cf883b32..000000000
--- a/src/Beef.Events.EventHubs/EventDataMapper.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-
-using Newtonsoft.Json;
-using System;
-using System.Text;
-using AzureEventHubs = Azure.Messaging.EventHubs;
-using MicrosoftEventHubs = Microsoft.Azure.EventHubs;
-
-namespace Beef.Events
-{
- ///
- /// Provides to / from mapping (as extension methods). Beef automatically adds automatically adds the properties.
- ///
- public static class EventDataMapper
- {
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The .
- public static EventData ToBeefEventData(this AzureEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- var body = Encoding.UTF8.GetString(eventData.EventBody);
- return OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject(body), eventData);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The value .
- /// The .
- /// The .
- public static EventData ToBeefEventData(this AzureEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- var body = Encoding.UTF8.GetString(eventData.EventBody);
- return (EventData)OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject>(body), eventData);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The value .
- /// The .
- public static EventData ToBeefEventData(this AzureEventHubs.EventData eventData, Type valueType)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- Beef.Check.NotNull(valueType, nameof(valueType));
-
- var body = Encoding.UTF8.GetString(eventData.EventBody);
- return OverrideKey((EventData)JsonConvert.DeserializeObject(body, typeof(EventData<>).MakeGenericType(new Type[] { valueType }))!, eventData);
- }
-
- ///
- /// Override the key - as the JSON serialized version loses the Type.
- ///
- private static EventData OverrideKey(EventData ed, AzureEventHubs.EventData eh)
- {
- if (eh.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
- ed.Key = key;
-
- return ed;
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- var body = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count);
- return OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject(body), eventData);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The value .
- /// The .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- var body = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count);
- return (EventData)OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject>(body), eventData);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The value .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftEventHubs.EventData eventData, Type valueType)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- Beef.Check.NotNull(valueType, nameof(valueType));
-
- var body = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count);
- return OverrideKey((EventData)JsonConvert.DeserializeObject(body, typeof(EventData<>).MakeGenericType(new Type[] { valueType }))!, eventData);
- }
-
- ///
- /// Override the key - as the JSON serialized version loses the Type.
- ///
- private static EventData OverrideKey(EventData ed, MicrosoftEventHubs.EventData eh)
- {
- if (eh.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
- ed.Key = key;
-
- return ed;
- }
-
- ///
- /// Converts the to a corresponding .
- ///
- /// The .
- /// The .
- public static MicrosoftEventHubs.EventData ToEventHubsEventData(this EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- if (string.IsNullOrEmpty(eventData.Subject))
- throw new ArgumentException("Subject property is required to be set.", nameof(eventData));
-
- var json = JsonConvert.SerializeObject(eventData);
- var bytes = Encoding.UTF8.GetBytes(json);
- var ed = new MicrosoftEventHubs.EventData(bytes);
-
- ed.Properties.Add(EventMetadata.EventIdPropertyName, eventData.EventId);
- ed.Properties.Add(EventMetadata.SubjectPropertyName, eventData.Subject);
- ed.Properties.Add(EventMetadata.ActionPropertyName, eventData.Action);
- ed.Properties.Add(EventMetadata.TenantIdPropertyName, eventData.TenantId);
- ed.Properties.Add(EventMetadata.KeyPropertyName, eventData.Key);
- ed.Properties.Add(EventMetadata.CorrelationIdPropertyName, eventData.CorrelationId);
- ed.Properties.Add(EventMetadata.PartitionKeyPropertyName, eventData.PartitionKey);
-
- return ed;
- }
-
- ///
- /// Converts the to a corresponding .
- ///
- /// The .
- /// The .
- public static AzureEventHubs.EventData ToAzureEventHubsEventData(this EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- if (string.IsNullOrEmpty(eventData.Subject))
- throw new ArgumentException("Subject property is required to be set.", nameof(eventData));
-
- var json = JsonConvert.SerializeObject(eventData);
- var bytes = Encoding.UTF8.GetBytes(json);
- var ed = new AzureEventHubs.EventData(bytes);
-
- ed.Properties.Add(EventMetadata.EventIdPropertyName, eventData.EventId);
- ed.Properties.Add(EventMetadata.SubjectPropertyName, eventData.Subject);
- ed.Properties.Add(EventMetadata.ActionPropertyName, eventData.Action);
- ed.Properties.Add(EventMetadata.TenantIdPropertyName, eventData.TenantId);
- ed.Properties.Add(EventMetadata.KeyPropertyName, eventData.Key);
- ed.Properties.Add(EventMetadata.CorrelationIdPropertyName, eventData.CorrelationId);
- ed.Properties.Add(EventMetadata.PartitionKeyPropertyName, eventData.PartitionKey);
-
- return ed;
- }
-
- ///
- /// Gets the from the .
- ///
- /// The .
- /// The values of the following properties: , , ,
- /// , , and .
- public static (Guid? EventId, string? Subject, string? Action, Guid? TenantId, string? CorrelationId, string? PartitionKey) GetBeefMetadata(this AzureEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- eventData.Properties.TryGetValue(EventMetadata.SubjectPropertyName, out var subject);
- eventData.Properties.TryGetValue(EventMetadata.ActionPropertyName, out var action);
-
- var eventId = (eventData.Properties.TryGetValue(EventMetadata.EventIdPropertyName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null;
- var tenantId = (eventData.Properties.TryGetValue(EventMetadata.TenantIdPropertyName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null;
- eventData.Properties.TryGetValue(EventMetadata.CorrelationIdPropertyName, out var correlationId);
- eventData.Properties.TryGetValue(EventMetadata.PartitionKeyPropertyName, out var partitionKey);
-
- return (eventId, (string?)subject, (string?)action, tenantId, (string?)correlationId, (string?)partitionKey);
- }
-
- ///
- /// Gets the Beef-related metadata from the .
- ///
- /// The .
- /// The .
- public static EventMetadata GetEventMetadata(this MicrosoftEventHubs.EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- eventData.Properties.TryGetValue(EventMetadata.SubjectPropertyName, out var subject);
- eventData.Properties.TryGetValue(EventMetadata.ActionPropertyName, out var action);
- eventData.Properties.TryGetValue(EventMetadata.CorrelationIdPropertyName, out var correlationId);
- eventData.Properties.TryGetValue(EventMetadata.PartitionKeyPropertyName, out var partitionKey);
-
- return new EventMetadata
- {
- EventId = (eventData.Properties.TryGetValue(EventMetadata.EventIdPropertyName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null,
- TenantId = (eventData.Properties.TryGetValue(EventMetadata.TenantIdPropertyName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
- Subject = (string?)subject,
- Action = (string?)action,
- CorrelationId = (string?)correlationId,
- PartitionKey = (string?)partitionKey
- };
- }
- }
-}
\ No newline at end of file
diff --git a/src/Beef.Events.EventHubs/EventHubConsumerHost.cs b/src/Beef.Events.EventHubs/EventHubConsumerHost.cs
index f7d961eb2..0a96ef56d 100644
--- a/src/Beef.Events.EventHubs/EventHubConsumerHost.cs
+++ b/src/Beef.Events.EventHubs/EventHubConsumerHost.cs
@@ -1,27 +1,20 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-using AzureEventHubs = Microsoft.Azure.EventHubs;
+using MicrosoftEventHubs = Microsoft.Azure.EventHubs;
namespace Beef.Events.EventHubs
{
///
- /// Provides the Azure Event Hubs (see ) .
+ /// Provides the Azure Event Hubs (see ) .
///
- public class EventHubConsumerHost : EventSubscriberHost
+ public class EventHubConsumerHost : EventSubscriberHost
{
///
/// Initializes a new instance of the with the specified .
///
/// The optional .
- public EventHubConsumerHost(EventSubscriberHostArgs args) : base(args) { }
-
- ///
- /// Gets the from the .
- ///
- /// The event/message data.
- /// The identified to process.
- /// The corresponding .
- protected override EventData GetBeefEventData(EventHubData data, IEventSubscriber subscriber)
- => subscriber.ValueType == null ? data.Originating.ToBeefEventData() : data.Originating.ToBeefEventData(subscriber.ValueType);
+ /// The optional . Defaults to a using a .
+ public EventHubConsumerHost(EventSubscriberHostArgs args, IEventDataConverter? eventDataConverter = null)
+ : base(args, eventDataConverter ?? new MicrosoftEventHubsEventConverter(new NewtonsoftJsonCloudEventSerializer())) { }
}
}
\ No newline at end of file
diff --git a/src/Beef.Events.EventHubs/EventHubData.cs b/src/Beef.Events.EventHubs/EventHubData.cs
index 73d7f02e0..9cfba4ddf 100644
--- a/src/Beef.Events.EventHubs/EventHubData.cs
+++ b/src/Beef.Events.EventHubs/EventHubData.cs
@@ -47,10 +47,5 @@ public EventHubData(AzureConsumer.PartitionContext partitionContext, AzureEventH
/// Gets or sets the Event Hubs partition identifier.
///
public string PartitionId { get; }
-
- ///
- /// Gets the metadata.
- ///
- protected override EventMetadata GetEventMetadata() => Originating.GetEventMetadata();
}
}
\ No newline at end of file
diff --git a/src/Beef.Events.EventHubs/EventExtensions.cs b/src/Beef.Events.EventHubs/EventHubExtensions.cs
similarity index 83%
rename from src/Beef.Events.EventHubs/EventExtensions.cs
rename to src/Beef.Events.EventHubs/EventHubExtensions.cs
index e8c02c67a..21e5923df 100644
--- a/src/Beef.Events.EventHubs/EventExtensions.cs
+++ b/src/Beef.Events.EventHubs/EventHubExtensions.cs
@@ -10,7 +10,7 @@ namespace Beef.Events.EventHubs
///
/// Provides the extensions methods for the events capabilities.
///
- public static class EventExtensions
+ public static class EventHubExtensions
{
///
/// Adds a transient service to instantiate a new instance using the specified .
@@ -51,18 +51,19 @@ public static IServiceCollection AddBeefEventHubConsumerHost(this IServiceCollec
/// Adds a scoped service to instantiate a new instance.
///
/// The .
- /// The connection string.
- /// The optional .
+ /// The .
+ /// Optyional (additional) opportunity to further configure the instantiated .
/// The for fluent-style method-chaining.
- public static IServiceCollection AddBeefEventHubEventProducer(this IServiceCollection services, string connectionString, EventHubProducerClientOptions? clientOptions = null)
+ public static IServiceCollection AddBeefEventHubEventProducer(this IServiceCollection services, EventHubProducerClient client, Action? additional = null)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
return services.AddScoped(_ =>
{
- var ehc = new EventHubProducerClient(Check.NotEmpty(connectionString, nameof(connectionString)), clientOptions);
- return new EventHubProducer(ehc);
+ var ehp = new EventHubProducer(Check.NotNull(client, nameof(client)));
+ additional?.Invoke(ehp);
+ return ehp;
});
}
}
diff --git a/src/Beef.Events.EventHubs/EventHubProducer.cs b/src/Beef.Events.EventHubs/EventHubProducer.cs
index d56c6ad55..b2318861c 100644
--- a/src/Beef.Events.EventHubs/EventHubProducer.cs
+++ b/src/Beef.Events.EventHubs/EventHubProducer.cs
@@ -10,8 +10,9 @@
namespace Beef.Events.EventHubs
{
///
- /// Send the array (converted to ) in multiple batches based on .
+ /// Send the array (converted to ) in multiple batches based on .
///
+ /// The and default to .
public class EventHubProducer : EventPublisherBase
{
private readonly EventHubProducerClient _client;
@@ -27,6 +28,34 @@ public EventHubProducer(EventHubProducerClient client, EventHubProducerInvoker?
{
_client = Check.NotNull(client, nameof(client));
_invoker = invoker ?? new EventHubProducerInvoker();
+ SubjectFormat = ActionFormat = EventStringFormat.Lowercase;
+ }
+
+ ///
+ /// Sets both the and to the specified .
+ ///
+ /// The .
+ /// This instance to support fluent-style method-chaining.
+ public EventHubProducer Format(EventStringFormat format)
+ {
+ SubjectFormat = ActionFormat = format;
+ return this;
+ }
+
+ ///
+ /// Gets or sets the . Defaults to using the .
+ ///
+ public IEventDataConverter? EventDataConverter { get; set; }
+
+ ///
+ /// Sets the .
+ ///
+ /// The
+ /// This instance to support fluent-style method-chaining.
+ public EventHubProducer SetEventDataConverter(IEventDataConverter? eventDataConverter)
+ {
+ EventDataConverter = eventDataConverter;
+ return this;
}
///
@@ -39,6 +68,8 @@ protected override async Task SendEventsAsync(params EventData[] events)
if (events == null || events.Length == 0)
return;
+ EventDataConverter ??= new AzureEventHubsEventConverter(new NewtonsoftJsonCloudEventSerializer());
+
// Why this logic: https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/eventhub/Azure.Messaging.EventHubs/samples/Sample04_PublishingEvents.md
EventDataBatch batch = null!;
var batches = new List();
@@ -50,7 +81,7 @@ protected override async Task SendEventsAsync(params EventData[] events)
batches.Add(batch = pk.Key == null ? await _client.CreateBatchAsync().ConfigureAwait(false) : await _client.CreateBatchAsync(new CreateBatchOptions { PartitionKey = pk.Key }).ConfigureAwait(false));
foreach (var ed in pk)
{
- var eh = ed.ToAzureEventHubsEventData();
+ var eh = await EventDataConverter.ConvertToAsync(ed).ConfigureAwait(false);
if (!batch.TryAdd(eh))
{
batches.Add(batch = pk.Key == null ? await _client.CreateBatchAsync().ConfigureAwait(false) : await _client.CreateBatchAsync(new CreateBatchOptions { PartitionKey = pk.Key }).ConfigureAwait(false));
diff --git a/src/Beef.Events.EventHubs/MicrosoftEventHubsEventConverter.cs b/src/Beef.Events.EventHubs/MicrosoftEventHubsEventConverter.cs
new file mode 100644
index 000000000..12b10d0bb
--- /dev/null
+++ b/src/Beef.Events.EventHubs/MicrosoftEventHubsEventConverter.cs
@@ -0,0 +1,170 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Threading.Tasks;
+using MicrosoftEventHubs = Microsoft.Azure.EventHubs;
+
+namespace Beef.Events.EventHubs
+{
+ ///
+ /// Represents an Azure Event Hubs (see ) converter.
+ ///
+ public sealed class MicrosoftEventHubsEventConverter : IEventDataConverter
+ {
+ private readonly IEventDataContentSerializer _contentSerializer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The . Defaults to .
+ public MicrosoftEventHubsEventConverter(IEventDataContentSerializer? contentSerializer = null) => _contentSerializer = contentSerializer ?? new NewtonsoftJsonCloudEventSerializer();
+
+ ///
+ /// Indicates whether to write to the for the .
+ ///
+ public bool UseMessagingPropertiesForMetadata { get; set; }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(MicrosoftEventHubs.EventData @event)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(@event.Body.ToArray()).ConfigureAwait(false)) ?? new EventData(null);
+ await MergeMetadataAndFixKeyAsync(ed, @event).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(Type valueType, MicrosoftEventHubs.EventData @event)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(valueType, @event.Body.ToArray()).ConfigureAwait(false)) ?? AzureEventHubsEventConverter.CreateValueEventData(valueType, await GetMetadataAsync(@event).ConfigureAwait(false));
+ await MergeMetadataAndFixKeyAsync(ed, @event).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Merge in the metadata and fix the key.
+ ///
+ private async Task MergeMetadataAndFixKeyAsync(EventData ed, MicrosoftEventHubs.EventData eh)
+ {
+ if (!UseMessagingPropertiesForMetadata)
+ return;
+
+ var md = await GetMetadataAsync(eh).ConfigureAwait(false);
+ if (md != null)
+ {
+ ed.MergeMetadata(md);
+
+ // Always override the key as it doesn't lose the type like the serialized value does.
+ if (eh.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
+ ed.Key = key;
+ }
+ }
+
+ ///
+ /// Converts an to a .
+ ///
+ /// The .
+ /// The .
+ public async Task ConvertToAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (string.IsNullOrEmpty(@event.Subject))
+ throw new ArgumentException("Subject property is required to be set.", nameof(@event));
+
+ var bytes = await _contentSerializer.SerializeAsync(@event).ConfigureAwait(false);
+ var ehed = new MicrosoftEventHubs.EventData(bytes);
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ ehed.Properties.Add(EventMetadata.SubjectAttributeName, @event.Subject);
+
+ if (@event.EventId != null)
+ ehed.Properties.Add(EventMetadata.EventIdAttributeName, @event.EventId);
+
+ if (@event.Action != null)
+ ehed.Properties.Add(EventMetadata.ActionAttributeName, @event.Action);
+
+ if (@event.TenantId != null)
+ ehed.Properties.Add(EventMetadata.TenantIdAttributeName, @event.TenantId);
+
+ if (@event.Source != null)
+ ehed.Properties.Add(EventMetadata.SourceAttributeName, @event.Source);
+
+ if (@event.Key != null)
+ ehed.Properties.Add(EventMetadata.KeyPropertyName, @event.Key);
+
+ if (@event.ETag != null)
+ ehed.Properties.Add(EventMetadata.ETagAttributeName, @event.ETag);
+
+ if (@event.Username != null)
+ ehed.Properties.Add(EventMetadata.UsernameAttributeName, @event.Username);
+
+ if (@event.UserId != null)
+ ehed.Properties.Add(EventMetadata.UserIdAttributeName, @event.UserId);
+
+ if (@event.Timestamp != null)
+ ehed.Properties.Add(EventMetadata.TimestampAttributeName, @event.Timestamp);
+
+ if (@event.CorrelationId != null)
+ ehed.Properties.Add(EventMetadata.CorrelationIdAttributeName, @event.CorrelationId);
+
+ if (@event.PartitionKey != null)
+ ehed.Properties.Add(EventMetadata.PartitionKeyAttributeName, @event.PartitionKey);
+ }
+
+ return ehed;
+ }
+
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ public async Task GetMetadataAsync(MicrosoftEventHubs.EventData message)
+ {
+ if (message == null)
+ return null;
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ message.Properties.TryGetValue(EventMetadata.SubjectAttributeName, out var subject);
+ message.Properties.TryGetValue(EventMetadata.ActionAttributeName, out var action);
+ message.Properties.TryGetValue(EventMetadata.CorrelationIdAttributeName, out var correlationId);
+ message.Properties.TryGetValue(EventMetadata.PartitionKeyAttributeName, out var partitionKey);
+ message.Properties.TryGetValue(EventMetadata.KeyPropertyName, out var key);
+ message.Properties.TryGetValue(EventMetadata.ETagAttributeName, out var etag);
+ message.Properties.TryGetValue(EventMetadata.UsernameAttributeName, out var username);
+ message.Properties.TryGetValue(EventMetadata.UserIdAttributeName, out var userId);
+
+ return new EventMetadata
+ {
+ EventId = (message.Properties.TryGetValue(EventMetadata.EventIdAttributeName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null,
+ TenantId = (message.Properties.TryGetValue(EventMetadata.TenantIdAttributeName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
+ Subject = (string?)subject,
+ Action = (string?)action,
+ Source = (message.Properties.TryGetValue(EventMetadata.SourceAttributeName, out var src) && src != null && src is Uri) ? (Uri?)src : null,
+ Key = key,
+ ETag = (string)etag,
+ Username = (string?)username,
+ UserId = (string?)userId,
+ Timestamp = (message.Properties.TryGetValue(EventMetadata.TimestampAttributeName, out var time) && time != null && time is DateTime?) ? (DateTime?)time : null,
+ CorrelationId = (string?)correlationId,
+ PartitionKey = (string?)partitionKey
+ };
+ }
+
+ // Try deserializing to get metadata where possible.
+ return await _contentSerializer.DeserializeAsync(message.Body.ToArray()).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/AzureServiceBusMessageConverter.cs b/src/Beef.Events.ServiceBus/AzureServiceBusMessageConverter.cs
new file mode 100644
index 000000000..ff2cce409
--- /dev/null
+++ b/src/Beef.Events.ServiceBus/AzureServiceBusMessageConverter.cs
@@ -0,0 +1,195 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using AzureServiceBus = Azure.Messaging.ServiceBus;
+
+namespace Beef.Events.ServiceBus
+{
+ ///
+ /// Represents an Azure Service Bus (see ) converter.
+ ///
+ public sealed class AzureServiceBusMessageConverter : IEventDataConverter
+ {
+ private readonly IEventDataContentSerializer _contentSerializer;
+
+ ///
+ /// Creates a new instance of using the specified .
+ ///
+ /// The .
+ /// The corresponding .
+ /// A new instance of
+ internal static EventData CreateValueEventData(Type valueType, EventMetadata? metadata) => (EventData)Activator.CreateInstance(typeof(EventData<>).MakeGenericType(new Type[] { valueType }), new object[] { metadata! });
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The . Defaults to .
+ /// Indicates whether to use the for .
+ public AzureServiceBusMessageConverter(IEventDataContentSerializer? contentSerializer = null, bool useAttributes = true) => _contentSerializer = contentSerializer ?? new NewtonsoftJsonCloudEventSerializer();
+
+ ///
+ /// Indicates whether to write to the for the .
+ ///
+ public bool UseMessagingPropertiesForMetadata { get; set; }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(AzureServiceBus.ServiceBusMessage message)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(message.Body.ToArray()).ConfigureAwait(false)) ?? new EventData(null);
+ await MergeMetadataAndFixKeyAsync(ed, message).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(Type valueType, AzureServiceBus.ServiceBusMessage message)
+ {
+ var ed = await (_contentSerializer.DeserializeAsync(valueType, message.Body.ToArray()).ConfigureAwait(false)) ?? CreateValueEventData(valueType, await GetMetadataAsync(message).ConfigureAwait(false));
+ await MergeMetadataAndFixKeyAsync(ed, message).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Merge in the metadata and fix the key.
+ ///
+ private async Task MergeMetadataAndFixKeyAsync(EventData ed, AzureServiceBus.ServiceBusMessage message)
+ {
+ if (!UseMessagingPropertiesForMetadata)
+ return;
+
+ var md = await GetMetadataAsync(message).ConfigureAwait(false);
+ if (md != null)
+ {
+ ed.MergeMetadata(md);
+
+ // Always override the key as it doesn't lose the type like the serialized value does.
+ if (message.ApplicationProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
+ ed.Key = key;
+ }
+ }
+
+ ///
+ /// Converts an to a .
+ ///
+ /// The .
+ /// The .
+ public async Task ConvertToAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (string.IsNullOrEmpty(@event.Subject))
+ throw new ArgumentException("Subject property is required to be set.", nameof(@event));
+
+ var bytes = await _contentSerializer.SerializeAsync(@event).ConfigureAwait(false);
+ var msg = new AzureServiceBus.ServiceBusMessage(new BinaryData(bytes))
+ {
+ Subject = @event.Subject,
+ ContentType = MediaTypeNames.Application.Json
+ };
+
+ if (@event.EventId != null)
+ msg.MessageId = @event.EventId?.ToString();
+
+ if (@event.CorrelationId != null)
+ msg.CorrelationId = @event.CorrelationId;
+
+ if (@event.PartitionKey != null)
+ msg.PartitionKey = @event.PartitionKey;
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ msg.ApplicationProperties.Add(EventMetadata.SubjectAttributeName, @event.Subject);
+
+ if (@event.EventId != null)
+ msg.ApplicationProperties.Add(EventMetadata.EventIdAttributeName, @event.EventId);
+
+ if (@event.Action != null)
+ msg.ApplicationProperties.Add(EventMetadata.ActionAttributeName, @event.Action);
+
+ if (@event.TenantId != null)
+ msg.ApplicationProperties.Add(EventMetadata.TenantIdAttributeName, @event.TenantId);
+
+ if (@event.Source != null)
+ msg.ApplicationProperties.Add(EventMetadata.SourceAttributeName, @event.Source);
+
+ if (@event.Key != null)
+ msg.ApplicationProperties.Add(EventMetadata.KeyPropertyName, @event.Key);
+
+ if (@event.ETag != null)
+ msg.ApplicationProperties.Add(EventMetadata.ETagAttributeName, @event.ETag);
+
+ if (@event.Username != null)
+ msg.ApplicationProperties.Add(EventMetadata.UsernameAttributeName, @event.Username);
+
+ if (@event.UserId != null)
+ msg.ApplicationProperties.Add(EventMetadata.UserIdAttributeName, @event.UserId);
+
+ if (@event.Timestamp != null)
+ msg.ApplicationProperties.Add(EventMetadata.TimestampAttributeName, @event.Timestamp);
+
+ if (@event.CorrelationId != null)
+ msg.ApplicationProperties.Add(EventMetadata.CorrelationIdAttributeName, @event.CorrelationId);
+
+ if (@event.PartitionKey != null)
+ msg.ApplicationProperties.Add(EventMetadata.PartitionKeyAttributeName, @event.PartitionKey);
+ }
+
+ return msg;
+ }
+
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ public async Task GetMetadataAsync(AzureServiceBus.ServiceBusMessage message)
+ {
+ if (message == null)
+ return null;
+
+ message.ApplicationProperties.TryGetValue(EventMetadata.SubjectAttributeName, out var subject);
+
+ // Where the Subject metadata is defined assume it is correctly configured.
+ if (subject != null)
+ {
+ message.ApplicationProperties.TryGetValue(EventMetadata.ActionAttributeName, out var action);
+ message.ApplicationProperties.TryGetValue(EventMetadata.CorrelationIdAttributeName, out var correlationId);
+ message.ApplicationProperties.TryGetValue(EventMetadata.PartitionKeyAttributeName, out var partitionKey);
+ message.ApplicationProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key);
+ message.ApplicationProperties.TryGetValue(EventMetadata.ETagAttributeName, out var etag);
+ message.ApplicationProperties.TryGetValue(EventMetadata.UsernameAttributeName, out var username);
+ message.ApplicationProperties.TryGetValue(EventMetadata.UserIdAttributeName, out var userId);
+
+ return new EventMetadata
+ {
+ EventId = (message.ApplicationProperties.TryGetValue(EventMetadata.EventIdAttributeName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : Guid.TryParse(message.MessageId, out var mid) ? mid : (Guid?)null,
+ TenantId = (message.ApplicationProperties.TryGetValue(EventMetadata.TenantIdAttributeName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
+ Subject = (string?)subject ?? message.Subject,
+ Action = (string?)action,
+ Source = (message.ApplicationProperties.TryGetValue(EventMetadata.SourceAttributeName, out var src) && src != null && src is Uri) ? (Uri?)src : null,
+ Key = key,
+ ETag = (string)etag,
+ Username = (string?)username,
+ UserId = (string?)userId,
+ Timestamp = (message.ApplicationProperties.TryGetValue(EventMetadata.TimestampAttributeName, out var time) && time != null && time is DateTime?) ? (DateTime?)time : null,
+ CorrelationId = (string?)correlationId ?? message.CorrelationId,
+ PartitionKey = (string?)partitionKey ?? message.PartitionKey
+ };
+ }
+
+ // Try deserializing to get metadata where possible.
+ return await _contentSerializer.DeserializeAsync(message.Body.ToArray()).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/Beef.Events.ServiceBus.csproj b/src/Beef.Events.ServiceBus/Beef.Events.ServiceBus.csproj
index 61a797ae1..77ac3aec5 100644
--- a/src/Beef.Events.ServiceBus/Beef.Events.ServiceBus.csproj
+++ b/src/Beef.Events.ServiceBus/Beef.Events.ServiceBus.csproj
@@ -2,7 +2,7 @@
netstandard2.1
- 4.1.1
+ 4.1.2Beef DevelopersAvanadeBusiness Entity Execution Framework (Beef) Service Bus framework.
diff --git a/src/Beef.Events.ServiceBus/CHANGELOG.md b/src/Beef.Events.ServiceBus/CHANGELOG.md
index a42df6e6c..78f1c72c5 100644
--- a/src/Beef.Events.ServiceBus/CHANGELOG.md
+++ b/src/Beef.Events.ServiceBus/CHANGELOG.md
@@ -2,5 +2,9 @@
Represents the **NuGet** versions.
+## v4.1.2
+- *Enhancement:* Support new `IEventDataContentSerializer` and `IEventDataConverter`.
+- *Enhancement:* Added `AzureServiceBusMessageConverter` and `MicrosoftServiceBusMessageConverter` for their respective, different, SDK versions.
+
## v4.1.1
- *New:* Initial publish to GitHub.
diff --git a/src/Beef.Events.ServiceBus/EventDataMapper.cs b/src/Beef.Events.ServiceBus/EventDataMapper.cs
deleted file mode 100644
index 717f5b22e..000000000
--- a/src/Beef.Events.ServiceBus/EventDataMapper.cs
+++ /dev/null
@@ -1,259 +0,0 @@
-// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-
-using Newtonsoft.Json;
-using System;
-using System.Text;
-using AzureServiceBus = Azure.Messaging.ServiceBus;
-using MicrosoftServiceBus = Microsoft.Azure.ServiceBus;
-
-namespace Beef.Events
-{
- ///
- /// Provides to / from mapping (as extension methods). Beef automatically adds the properties.
- ///
- public static class EventDataMapper
- {
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The .
- public static EventData ToBeefEventData(this AzureServiceBus.ServiceBusMessage message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject(body), message);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The value .
- /// The .
- /// The .
- public static EventData ToBeefEventData(this AzureServiceBus.ServiceBusMessage message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return (EventData)OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject>(body), message);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The value .
- /// The .
- public static EventData ToBeefEventData(this AzureServiceBus.ServiceBusMessage message, Type valueType)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- Beef.Check.NotNull(valueType, nameof(valueType));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return OverrideKey((EventData)JsonConvert.DeserializeObject(body, typeof(EventData<>).MakeGenericType(new Type[] { valueType }))!, message);
- }
-
- ///
- /// Override the key - as the JSON serialized version loses the Type.
- ///
- private static EventData OverrideKey(EventData ed, AzureServiceBus.ServiceBusMessage msg)
- {
- if (msg.ApplicationProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
- ed.Key = key;
-
- return ed;
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftServiceBus.Message message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject(body), message);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The value .
- /// The .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftServiceBus.Message message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return (EventData)OverrideKey(Newtonsoft.Json.JsonConvert.DeserializeObject>(body), message);
- }
-
- ///
- /// Converts the instance to the corresponding .
- ///
- /// The .
- /// The value .
- /// The .
- public static EventData ToBeefEventData(this MicrosoftServiceBus.Message message, Type valueType)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- Beef.Check.NotNull(valueType, nameof(valueType));
-
- var body = Encoding.UTF8.GetString(message.Body);
- return OverrideKey((EventData)JsonConvert.DeserializeObject(body, typeof(EventData<>).MakeGenericType(new Type[] { valueType }))!, message);
- }
-
- ///
- /// Override the key - as the JSON serialized version loses the Type.
- ///
- private static EventData OverrideKey(EventData ed, MicrosoftServiceBus.Message msg)
- {
- if (msg.UserProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
- ed.Key = key;
-
- return ed;
- }
-
- ///
- /// Converts the to a corresponding .
- ///
- /// The .
- /// The .
- public static AzureServiceBus.ServiceBusMessage ToAzureServiceBusMessage(this EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- if (string.IsNullOrEmpty(eventData.Subject))
- throw new ArgumentException("Subject property is required to be set.", nameof(eventData));
-
- var json = JsonConvert.SerializeObject(eventData);
- var bytes = Encoding.UTF8.GetBytes(json);
- var msg = new AzureServiceBus.ServiceBusMessage(bytes)
- {
- Subject = eventData.Subject,
- ContentType = "Beef.Events.EventData"
- };
-
- if (eventData.EventId != null)
- msg.MessageId = eventData.EventId?.ToString();
-
- if (eventData.CorrelationId != null)
- msg.CorrelationId = eventData.CorrelationId;
-
- if (eventData.PartitionKey != null)
- msg.PartitionKey = eventData.PartitionKey;
-
- msg.ApplicationProperties.Add(EventMetadata.EventIdPropertyName, eventData.EventId);
- msg.ApplicationProperties.Add(EventMetadata.SubjectPropertyName, eventData.Subject);
- msg.ApplicationProperties.Add(EventMetadata.ActionPropertyName, eventData.Action);
- msg.ApplicationProperties.Add(EventMetadata.TenantIdPropertyName, eventData.TenantId);
- msg.ApplicationProperties.Add(EventMetadata.KeyPropertyName, eventData.Key);
- msg.ApplicationProperties.Add(EventMetadata.CorrelationIdPropertyName, eventData.CorrelationId);
- msg.ApplicationProperties.Add(EventMetadata.PartitionKeyPropertyName, eventData.PartitionKey);
-
- return msg;
- }
-
- ///
- /// Converts the to a corresponding .
- ///
- /// The .
- /// The .
- public static MicrosoftServiceBus.Message ToServiceBusMessage(this EventData eventData)
- {
- if (eventData == null)
- throw new ArgumentNullException(nameof(eventData));
-
- if (string.IsNullOrEmpty(eventData.Subject))
- throw new ArgumentException("Subject property is required to be set.", nameof(eventData));
-
- var json = JsonConvert.SerializeObject(eventData);
- var bytes = Encoding.UTF8.GetBytes(json);
- var msg = new MicrosoftServiceBus.Message(bytes)
- {
- ContentType = "Beef.Events.EventData"
- };
-
- if (eventData.EventId != null)
- msg.MessageId = eventData.EventId?.ToString();
-
- if (eventData.CorrelationId != null)
- msg.CorrelationId = eventData.CorrelationId;
-
- if (eventData.PartitionKey != null)
- msg.PartitionKey = eventData.PartitionKey;
-
- msg.UserProperties.Add(EventMetadata.EventIdPropertyName, eventData.EventId);
- msg.UserProperties.Add(EventMetadata.SubjectPropertyName, eventData.Subject);
- msg.UserProperties.Add(EventMetadata.ActionPropertyName, eventData.Action);
- msg.UserProperties.Add(EventMetadata.TenantIdPropertyName, eventData.TenantId);
- msg.UserProperties.Add(EventMetadata.KeyPropertyName, eventData.Key);
- msg.UserProperties.Add(EventMetadata.CorrelationIdPropertyName, eventData.CorrelationId);
- msg.UserProperties.Add(EventMetadata.PartitionKeyPropertyName, eventData.PartitionKey);
-
- return msg;
- }
-
- ///
- /// Gets the Beef-related metadata from the .
- ///
- /// The .
- /// The values of the following properties: , , ,
- /// , , and .
- public static (Guid? EventId, string? Subject, string? Action, Guid? TenantId, string? CorrelationId, string? PartitionKey) GetBeefMetadata(this AzureServiceBus.ServiceBusMessage message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- message.ApplicationProperties.TryGetValue(EventMetadata.SubjectPropertyName, out var subject);
- message.ApplicationProperties.TryGetValue(EventMetadata.ActionPropertyName, out var action);
-
- var eventId = (message.ApplicationProperties.TryGetValue(EventMetadata.EventIdPropertyName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null;
- var tenantId = (message.ApplicationProperties.TryGetValue(EventMetadata.TenantIdPropertyName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null;
- message.ApplicationProperties.TryGetValue(EventMetadata.CorrelationIdPropertyName, out var correlationId);
- message.ApplicationProperties.TryGetValue(EventMetadata.PartitionKeyPropertyName, out var partitionKey);
-
- return (eventId, (string?)subject, (string?)action, tenantId, (string?)correlationId, (string?)partitionKey);
- }
-
- ///
- /// Gets the from the .
- ///
- /// The .
- /// The .
- public static EventMetadata GetEventMetadata(this MicrosoftServiceBus.Message message)
- {
- if (message == null)
- throw new ArgumentNullException(nameof(message));
-
- message.UserProperties.TryGetValue(EventMetadata.SubjectPropertyName, out var subject);
- message.UserProperties.TryGetValue(EventMetadata.ActionPropertyName, out var action);
- message.UserProperties.TryGetValue(EventMetadata.CorrelationIdPropertyName, out var correlationId);
- message.UserProperties.TryGetValue(EventMetadata.PartitionKeyPropertyName, out var partitionKey);
-
- return new EventMetadata
- {
- EventId = (message.UserProperties.TryGetValue(EventMetadata.EventIdPropertyName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : null,
- TenantId = (message.UserProperties.TryGetValue(EventMetadata.TenantIdPropertyName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
- Subject = (string?)subject,
- Action = (string?)action,
- CorrelationId = (string?)correlationId,
- PartitionKey = (string?)partitionKey
- };
- }
- }
-}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/MicrosoftServiceBusMessageConverter.cs b/src/Beef.Events.ServiceBus/MicrosoftServiceBusMessageConverter.cs
new file mode 100644
index 000000000..9e74ea4e1
--- /dev/null
+++ b/src/Beef.Events.ServiceBus/MicrosoftServiceBusMessageConverter.cs
@@ -0,0 +1,186 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using MicrosoftServiceBus = Microsoft.Azure.ServiceBus;
+
+namespace Beef.Events.ServiceBus
+{
+ ///
+ /// Represents an Azure Service Bus (see ) converter.
+ ///
+ public sealed class MicrosoftServiceBusMessageConverter : IEventDataConverter
+ {
+ private readonly IEventDataContentSerializer _contentSerializer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The . Defaults to .
+ public MicrosoftServiceBusMessageConverter(IEventDataContentSerializer? contentSerializer = null) => _contentSerializer = contentSerializer ?? new NewtonsoftJsonCloudEventSerializer();
+
+ ///
+ /// Indicates whether to write to the for the .
+ ///
+ public bool UseMessagingPropertiesForMetadata { get; set; }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(MicrosoftServiceBus.Message message)
+ {
+ var ed = (await _contentSerializer.DeserializeAsync(message.Body).ConfigureAwait(false)) ?? new EventData(null);
+ await MergeMetadataAndFixKeyAsync(ed, message).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Converts a to an .
+ ///
+ /// The .
+ /// The .
+ /// The converted .
+ public async Task ConvertFromAsync(Type valueType, MicrosoftServiceBus.Message message)
+ {
+ var ed = await (_contentSerializer.DeserializeAsync(valueType, message.Body).ConfigureAwait(false)) ?? AzureServiceBusMessageConverter.CreateValueEventData(valueType, await GetMetadataAsync(message).ConfigureAwait(false));
+ await MergeMetadataAndFixKeyAsync(ed, message).ConfigureAwait(false);
+ return ed;
+ }
+
+ ///
+ /// Merge in the metadata and fix the key.
+ ///
+ private async Task MergeMetadataAndFixKeyAsync(EventData ed, MicrosoftServiceBus.Message message)
+ {
+ if (!UseMessagingPropertiesForMetadata)
+ return;
+
+ var md = await GetMetadataAsync(message).ConfigureAwait(false);
+ if (md != null)
+ {
+ ed.MergeMetadata(md);
+
+ // Always override the key as it doesn't lose the type like the serialized value does.
+ if (message.UserProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key))
+ ed.Key = key;
+ }
+ }
+
+ ///
+ /// Converts an to a .
+ ///
+ /// The .
+ /// The .
+ public async Task ConvertToAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (string.IsNullOrEmpty(@event.Subject))
+ throw new ArgumentException("Subject property is required to be set.", nameof(@event));
+
+ var bytes = await _contentSerializer.SerializeAsync(@event).ConfigureAwait(false);
+ var msg = new MicrosoftServiceBus.Message(bytes)
+ {
+ Label = @event.Subject,
+ ContentType = MediaTypeNames.Application.Json
+ };
+
+ if (@event.EventId != null)
+ msg.MessageId = @event.EventId?.ToString();
+
+ if (@event.CorrelationId != null)
+ msg.CorrelationId = @event.CorrelationId;
+
+ if (@event.PartitionKey != null)
+ msg.PartitionKey = @event.PartitionKey;
+
+ if (UseMessagingPropertiesForMetadata)
+ {
+ msg.UserProperties.Add(EventMetadata.SubjectAttributeName, @event.Subject);
+
+ if (@event.EventId != null)
+ msg.UserProperties.Add(EventMetadata.EventIdAttributeName, @event.EventId);
+
+ if (@event.Action != null)
+ msg.UserProperties.Add(EventMetadata.ActionAttributeName, @event.Action);
+
+ if (@event.TenantId != null)
+ msg.UserProperties.Add(EventMetadata.TenantIdAttributeName, @event.TenantId);
+
+ if (@event.Source != null)
+ msg.UserProperties.Add(EventMetadata.SourceAttributeName, @event.Source);
+
+ if (@event.Key != null)
+ msg.UserProperties.Add(EventMetadata.KeyPropertyName, @event.Key);
+
+ if (@event.ETag != null)
+ msg.UserProperties.Add(EventMetadata.ETagAttributeName, @event.ETag);
+
+ if (@event.Username != null)
+ msg.UserProperties.Add(EventMetadata.UsernameAttributeName, @event.Username);
+
+ if (@event.UserId != null)
+ msg.UserProperties.Add(EventMetadata.UserIdAttributeName, @event.UserId);
+
+ if (@event.Timestamp != null)
+ msg.UserProperties.Add(EventMetadata.TimestampAttributeName, @event.Timestamp);
+
+ if (@event.CorrelationId != null)
+ msg.UserProperties.Add(EventMetadata.CorrelationIdAttributeName, @event.CorrelationId);
+
+ if (@event.PartitionKey != null)
+ msg.UserProperties.Add(EventMetadata.PartitionKeyAttributeName, @event.PartitionKey);
+ }
+
+ return msg;
+ }
+
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ public async Task GetMetadataAsync(MicrosoftServiceBus.Message message)
+ {
+ if (message == null)
+ return null;
+
+ message.UserProperties.TryGetValue(EventMetadata.SubjectAttributeName, out var subject);
+
+ // Where the Subject metadata is defined assume it is correctly configured.
+ if (subject != null)
+ {
+ message.UserProperties.TryGetValue(EventMetadata.ActionAttributeName, out var action);
+ message.UserProperties.TryGetValue(EventMetadata.CorrelationIdAttributeName, out var correlationId);
+ message.UserProperties.TryGetValue(EventMetadata.PartitionKeyAttributeName, out var partitionKey);
+ message.UserProperties.TryGetValue(EventMetadata.KeyPropertyName, out var key);
+ message.UserProperties.TryGetValue(EventMetadata.ETagAttributeName, out var etag);
+ message.UserProperties.TryGetValue(EventMetadata.UsernameAttributeName, out var username);
+ message.UserProperties.TryGetValue(EventMetadata.UserIdAttributeName, out var userId);
+
+ return new EventMetadata
+ {
+ EventId = (message.UserProperties.TryGetValue(EventMetadata.EventIdAttributeName, out var eid) && eid != null && eid is Guid?) ? (Guid?)eid : Guid.TryParse(message.MessageId, out var mid) ? mid : (Guid?)null,
+ TenantId = (message.UserProperties.TryGetValue(EventMetadata.TenantIdAttributeName, out var tid) && tid != null && tid is Guid?) ? (Guid?)tid : null,
+ Subject = (string?)subject ?? message.Label,
+ Action = (string?)action,
+ Source = (message.UserProperties.TryGetValue(EventMetadata.SourceAttributeName, out var src) && src != null && src is Uri) ? (Uri?)src : null,
+ Key = key,
+ ETag = (string)etag,
+ Username = (string?)username,
+ UserId = (string?)userId,
+ Timestamp = (message.UserProperties.TryGetValue(EventMetadata.TimestampAttributeName, out var time) && time != null && time is DateTime?) ? (DateTime?)time : null,
+ CorrelationId = (string?)correlationId ?? message.CorrelationId,
+ PartitionKey = (string?)partitionKey ?? message.PartitionKey
+ };
+ }
+
+ // Try deserializing to get metadata where possible.
+ return await _contentSerializer.DeserializeAsync(message.Body).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/ServiceBusData.cs b/src/Beef.Events.ServiceBus/ServiceBusData.cs
index 25772f7dc..b7b375f47 100644
--- a/src/Beef.Events.ServiceBus/ServiceBusData.cs
+++ b/src/Beef.Events.ServiceBus/ServiceBusData.cs
@@ -30,10 +30,5 @@ public ServiceBusData(string serviceBusName, string queueName, AzureServiceBus.M
/// Gets the Service Bus Queue name.
///
public string QueueName { get; }
-
- ///
- /// Gets the metadata.
- ///
- protected override EventMetadata GetEventMetadata() => Originating.GetEventMetadata();
}
}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/EventExtensions.cs b/src/Beef.Events.ServiceBus/ServiceBusExtensions.cs
similarity index 71%
rename from src/Beef.Events.ServiceBus/EventExtensions.cs
rename to src/Beef.Events.ServiceBus/ServiceBusExtensions.cs
index b800ad28d..2be06e20e 100644
--- a/src/Beef.Events.ServiceBus/EventExtensions.cs
+++ b/src/Beef.Events.ServiceBus/ServiceBusExtensions.cs
@@ -10,7 +10,7 @@ namespace Beef.Events.ServiceBus
///
/// Provides the extensions methods for the events capabilities.
///
- public static class EventExtensions
+ public static class ServiceBusExtensions
{
///
/// Adds a transient service to instantiate a new instance using the specified .
@@ -48,41 +48,43 @@ public static IServiceCollection AddBeefServiceBusReceiverHost(this IServiceColl
}
///
- /// Adds a scoped service to instantiate a new instance where the quere will be inferred from the .
+ /// Adds a scoped service to instantiate a new using a where the quere will be inferred from the corresponding .
///
/// The .
- /// The connection string.
- /// The optional .
+ /// The .
+ /// Optyional (additional) opportunity to further configure the instantiated .
/// The for fluent-style method-chaining.
- public static IServiceCollection AddBeefEventServiceBusSender(this IServiceCollection services, string connectionString, ServiceBusClientOptions? clientOptions = null)
+ public static IServiceCollection AddBeefEventServiceBusSender(this IServiceCollection services, ServiceBusClient client, Action? additional = null)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
return services.AddScoped(_ =>
{
- var sbc = new ServiceBusClient(Check.NotEmpty(connectionString, nameof(connectionString)), clientOptions);
- return new ServiceBusSender(sbc);
+ var sbs = new ServiceBusSender(Check.NotNull(client, nameof(client)));
+ additional?.Invoke(sbs);
+ return sbs;
});
}
///
- /// Adds a scoped service to instantiate a new instance using the specified .
+ /// Adds a scoped service to instantiate a new using a instance with the specified .
///
/// The .
- /// The connection string.
+ /// The .
/// The queue name.
- /// The optional .
+ /// Optyional (additional) opportunity to further configure the instantiated .
/// The for fluent-style method-chaining.
- public static IServiceCollection AddBeefEventServiceBusSender(this IServiceCollection services, string connectionString, string queueName, ServiceBusClientOptions? clientOptions = null)
+ public static IServiceCollection AddBeefEventServiceBusSender(this IServiceCollection services, ServiceBusClient client, string queueName, Action? additional = null)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
return services.AddScoped(_ =>
{
- var sbc = new ServiceBusClient(Check.NotEmpty(connectionString, nameof(connectionString)), clientOptions);
- return new ServiceBusSender(sbc, queueName);
+ var sbs = new ServiceBusSender(Check.NotNull(client, nameof(client)), Check.NotEmpty(queueName, nameof(queueName)));
+ additional?.Invoke(sbs);
+ return sbs;
});
}
}
diff --git a/src/Beef.Events.ServiceBus/ServiceBusReceiverHost.cs b/src/Beef.Events.ServiceBus/ServiceBusReceiverHost.cs
index 93951c4ef..0ce965557 100644
--- a/src/Beef.Events.ServiceBus/ServiceBusReceiverHost.cs
+++ b/src/Beef.Events.ServiceBus/ServiceBusReceiverHost.cs
@@ -1,27 +1,20 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-using AzureServiceBus = Microsoft.Azure.ServiceBus;
+using MicrosoftServiceBus = Microsoft.Azure.ServiceBus;
namespace Beef.Events.ServiceBus
{
///
- /// Provides the Azure Service Bus (see ) .
+ /// Provides the Azure Service Bus (see ) .
///
- public class ServiceBusReceiverHost : EventSubscriberHost
+ public class ServiceBusReceiverHost : EventSubscriberHost
{
///
/// Initializes a new instance of the with the specified .
///
- /// The optional .
- public ServiceBusReceiverHost(EventSubscriberHostArgs args) : base(args) { }
-
- ///
- /// Gets the from the .
- ///
- /// The event/message data.
- /// The identified to process.
- /// The corresponding .
- protected override EventData GetBeefEventData(ServiceBusData data, IEventSubscriber subscriber)
- => subscriber.ValueType == null ? data.Originating.ToBeefEventData() : data.Originating.ToBeefEventData(subscriber.ValueType);
+ /// The .
+ /// The optional . Defaults to a using a .
+ public ServiceBusReceiverHost(EventSubscriberHostArgs args, IEventDataConverter? eventDataConverter = null)
+ : base(args, eventDataConverter ?? new MicrosoftServiceBusMessageConverter(new NewtonsoftJsonCloudEventSerializer())) { }
}
}
\ No newline at end of file
diff --git a/src/Beef.Events.ServiceBus/ServiceBusSender.cs b/src/Beef.Events.ServiceBus/ServiceBusSender.cs
index 67a4b3cd2..ddb00ac14 100644
--- a/src/Beef.Events.ServiceBus/ServiceBusSender.cs
+++ b/src/Beef.Events.ServiceBus/ServiceBusSender.cs
@@ -11,21 +11,27 @@ namespace Beef.Events.ServiceBus
///
/// Send the array (converted to ) in multiple batches based on .
///
+ /// The and default to .
public class ServiceBusSender : EventPublisherBase
{
private readonly AzureServiceBus.ServiceBusClient _client;
+ private readonly bool _removeKeyFromSubject;
private readonly ServiceBusSenderInvoker _invoker;
///
- /// Initializes a new instance of the using the specified where the queue will be inferred from the
+ /// Initializes a new instance of the using the specified where the queue will be inferred from the
/// using (consider setting the underlying ) to allow for transient errors).
///
/// The .
- /// Enables the to be overridden; defaults to .
- public ServiceBusSender(AzureServiceBus.ServiceBusClient client, ServiceBusSenderInvoker? invoker = null)
+ /// Indicates whether to remove the key queue name from the . This is achieved by removing the last part (typically the key) to provide the base path;
+ /// for example a Subject of Beef.Demo.Person.1234 would result in Beef.Demo.Person.
+ /// Enables the to be overridden. Defaults to .
+ public ServiceBusSender(AzureServiceBus.ServiceBusClient client, bool removeKeyFromSubject = false, ServiceBusSenderInvoker? invoker = null)
{
_client = Check.NotNull(client, nameof(client));
+ _removeKeyFromSubject = removeKeyFromSubject;
_invoker = invoker ?? new ServiceBusSenderInvoker();
+ SubjectFormat = ActionFormat = EventStringFormat.Lowercase;
}
///
@@ -35,13 +41,41 @@ public ServiceBusSender(AzureServiceBus.ServiceBusClient client, ServiceBusSende
/// The .
/// The queue name.
/// Enables the to be overridden; defaults to .
- public ServiceBusSender(AzureServiceBus.ServiceBusClient client, string queueName, ServiceBusSenderInvoker? invoker = null) : this(client, invoker) => QueueName = Check.NotEmpty(queueName, nameof(queueName));
+ public ServiceBusSender(AzureServiceBus.ServiceBusClient client, string queueName, ServiceBusSenderInvoker? invoker = null)
+ : this(client, false, invoker) => QueueName = Check.NotEmpty(queueName, nameof(queueName));
///
/// Gets the queue name. Where null this indicates that the queue name will be created (inferred) at runtime
///
public string? QueueName { get; private set; }
+ ///
+ /// Gets or sets the . Defaults to using the .
+ ///
+ public IEventDataConverter? EventDataConverter { get; set; }
+
+ ///
+ /// Sets the .
+ ///
+ /// The
+ /// This instance to support fluent-style method-chaining.
+ public ServiceBusSender SetEventDataConverter(IEventDataConverter? eventDataConverter)
+ {
+ EventDataConverter = eventDataConverter;
+ return this;
+ }
+
+ ///
+ /// Sets both the and to the specified .
+ ///
+ /// The .
+ /// This instance to support fluent-style method-chaining.
+ public ServiceBusSender SetFormat(EventStringFormat format)
+ {
+ SubjectFormat = ActionFormat = format;
+ return this;
+ }
+
///
///
///
@@ -52,17 +86,19 @@ protected override async Task SendEventsAsync(params EventData[] events)
if (events == null || events.Length == 0)
return;
+ EventDataConverter ??= new AzureServiceBusMessageConverter(new NewtonsoftJsonCloudEventSerializer());
+
// Why this logic: https://github.com/Azure/azure-sdk-for-net/tree/Azure.Messaging.ServiceBus_7.1.0/sdk/servicebus/Azure.Messaging.ServiceBus/#send-and-receive-a-batch-of-messages
var dict = new Dictionary>();
foreach (var @event in events)
{
var queueName = QueueName ?? CreateQueueName(@event);
if (dict.TryGetValue(queueName, out var list))
- list.Enqueue(@event.ToAzureServiceBusMessage());
+ list.Enqueue(await EventDataConverter.ConvertToAsync(@event).ConfigureAwait(false));
else
{
var queue = new Queue();
- queue.Enqueue(@event.ToAzureServiceBusMessage());
+ queue.Enqueue(await EventDataConverter.ConvertToAsync(@event).ConfigureAwait(false));
dict.Add(queueName, queue);
}
}
@@ -92,7 +128,7 @@ protected override async Task SendEventsAsync(params EventData[] events)
}
///
- /// Creates the queue name from the . This is achieved by removing the last part (typically the key) to provide the base path; for example a Subject of
+ /// Creates the queue name from the . This is achieved by removing the last part (typically the key) to provide the base path; for example a Subject of
/// Beef.Demo.Person.1234 would result in Beef.Demo.Person.
///
/// The .
@@ -105,6 +141,9 @@ public virtual string CreateQueueName(EventData @event)
if (string.IsNullOrEmpty(@event.Subject))
throw new ArgumentException("The Subject property must be specified.", nameof(@event));
+ if (!_removeKeyFromSubject)
+ return @event.Subject;
+
var parts = @event.Subject.Split(PathSeparator);
if (parts.Length <= 1)
return @event.Subject;
diff --git a/src/Beef.Events/Beef.Events.csproj b/src/Beef.Events/Beef.Events.csproj
index d8a2926dd..535db99c7 100644
--- a/src/Beef.Events/Beef.Events.csproj
+++ b/src/Beef.Events/Beef.Events.csproj
@@ -2,7 +2,7 @@
netstandard2.1
- 4.1.6
+ 4.1.7Beef DevelopersAvanadeBusiness Entity Execution Framework (Beef) Events framework.
@@ -28,11 +28,13 @@
+
+
diff --git a/src/Beef.Events/CHANGELOG.md b/src/Beef.Events/CHANGELOG.md
index 108fb3496..b4e00e206 100644
--- a/src/Beef.Events/CHANGELOG.md
+++ b/src/Beef.Events/CHANGELOG.md
@@ -2,6 +2,11 @@
Represents the **NuGet** versions.
+## v4.1.7
+- *Enhancement:* Added new `IEventDataContentSerializer` and `IEventDataConverter` to more easily facilitate multiple serializers and converters over time.
+- *Enhancement:* Leverage `IEventDataContentSerializer` to support [CloudEvents](https://github.com/cloudevents/sdk-csharp) with new `NewtonsoftJsonCloudEventSerializer`. This is the default.
+- *Enhancement:* Leverage `IEventDataContentSerializer` to support existing `EventData` format with `NewtonsoftJsonEventDataSerializer` for backwards compatibility.
+
## v4.1.6
- *Enhancement:* Added new `EventMetadata` class to house the _Beef_ metadata property names.
diff --git a/src/Beef.Events/EventDataConverter.cs b/src/Beef.Events/EventDataConverter.cs
new file mode 100644
index 000000000..66d8d20b9
--- /dev/null
+++ b/src/Beef.Events/EventDataConverter.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Threading.Tasks;
+
+namespace Beef.Events
+{
+ ///
+ /// Represents an to pass-through converter.
+ ///
+ public class EventDataConverter : IEventDataConverter
+ {
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task ConvertFromAsync(EventData @event) => Task.FromResult(@event);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task ConvertFromAsync(Type valueType, EventData @event) => Task.FromResult(@event);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task ConvertToAsync(EventData @event) => Task.FromResult(@event);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task GetMetadataAsync(EventData @event) => Task.FromResult(@event);
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events/EventDataSubscriberData.cs b/src/Beef.Events/EventDataSubscriberData.cs
index ef23547f5..e2cea89a8 100644
--- a/src/Beef.Events/EventDataSubscriberData.cs
+++ b/src/Beef.Events/EventDataSubscriberData.cs
@@ -12,21 +12,5 @@ public class EventDataSubscriberData : EventSubscriberData
///
/// The originating event/message.
public EventDataSubscriberData(EventData originating) : base(originating) { }
-
- ///
- ///
- ///
- ///
- protected override EventMetadata GetEventMetadata() =>
- new EventMetadata
- {
- EventId = Originating.EventId,
- Subject = Originating.Subject,
- Action = Originating.Action,
- TenantId = Originating.TenantId,
- Key = Originating.Key,
- PartitionKey = Originating.PartitionKey,
- CorrelationId = Originating.CorrelationId
- };
}
-}
+}
\ No newline at end of file
diff --git a/src/Beef.Events/EventDataSubscriberHost.cs b/src/Beef.Events/EventDataSubscriberHost.cs
index b9717f0cf..829a7ea92 100644
--- a/src/Beef.Events/EventDataSubscriberHost.cs
+++ b/src/Beef.Events/EventDataSubscriberHost.cs
@@ -14,7 +14,7 @@ public class EventDataSubscriberHost : EventSubscriberHost.
///
/// The .
- public EventDataSubscriberHost(EventSubscriberHostArgs args) : base(args) { }
+ public EventDataSubscriberHost(EventSubscriberHostArgs args) : base(args, new EventDataConverter()) { }
///
/// Performs the receive processing for one or more instances.
@@ -33,16 +33,8 @@ public async Task ReceiveAsync(params EventData[] events)
foreach (var @event in events)
{
- await ReceiveAsync(new EventDataSubscriberData(@event), (_) => @event).ConfigureAwait(false);
+ await ReceiveAsync(new EventDataSubscriberData(@event), (_) => Task.FromResult(@event)).ConfigureAwait(false);
}
}
-
- ///
- ///
- ///
- /// The event/message data.
- /// The identified to process.
- ///
- protected override EventData GetBeefEventData(EventDataSubscriberData data, IEventSubscriber subscriber) => data.Originating;
}
}
\ No newline at end of file
diff --git a/src/Beef.Events/EventMetadata.cs b/src/Beef.Events/EventMetadata.cs
deleted file mode 100644
index 3857d109b..000000000
--- a/src/Beef.Events/EventMetadata.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-
-using System;
-
-namespace Beef.Events
-{
- ///
- /// Provides the Beef metadata property names.
- ///
- public class EventMetadata
- {
- ///
- /// Gets or sets the EventId property name.
- ///
- public static string EventIdPropertyName { get; set; } = "Beef.EventId";
-
- ///
- /// Gets or sets the Subject property name.
- ///
- public static string SubjectPropertyName { get; set; } = "Beef.Subject";
-
- ///
- /// Gets or sets the Action property name.
- ///
- public static string ActionPropertyName { get; set; } = "Beef.Action";
-
- ///
- /// Gets or sets the TenantId property name.
- ///
- public static string TenantIdPropertyName { get; set; } = "Beef.TenantId";
-
- ///
- /// Gets or sets the Key property name.
- ///
- public static string KeyPropertyName { get; set; } = "Beef.Key";
-
- ///
- /// Gets or sets the CorrelationId property name.
- ///
- public static string CorrelationIdPropertyName { get; set; } = "Beef.CorrelationId";
-
- ///
- /// Gets or sets the PartitionKey property name.
- ///
- public static string PartitionKeyPropertyName { get; set; } = "Beef.PartitionKey";
-
- ///
- /// Gets or sets the unique event identifier.
- ///
- public Guid? EventId { get; set; }
-
- ///
- /// Gets or sets the tenant identifier.
- ///
- public Guid? TenantId { get; set; }
-
- ///
- /// Gets or sets the event subject (the name should use the '.' character to denote paths).
- ///
- public string? Subject { get; set; }
-
- ///
- /// Gets or sets the event action.
- ///
- public string? Action { get; set; }
-
- ///
- /// Gets or sets the entity key (could be single value or an array of values).
- ///
- public object? Key { get; set; }
-
- ///
- /// Gets or sets the correlation identifier.
- ///
- public string? CorrelationId { get; set; }
-
- ///
- /// Gets or sets the partition key.
- ///
- public string? PartitionKey { get; set; }
- }
-}
\ No newline at end of file
diff --git a/src/Beef.Events/EventSubscriberAttribute.cs b/src/Beef.Events/EventSubscriberAttribute.cs
index 6859c671b..150552804 100644
--- a/src/Beef.Events/EventSubscriberAttribute.cs
+++ b/src/Beef.Events/EventSubscriberAttribute.cs
@@ -8,14 +8,14 @@ namespace Beef.Events
///
/// Details the and for an .
///
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
- public class EventSubscriberAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ public sealed class EventSubscriberAttribute : Attribute
{
///
/// Initializes a new instance of the class.
///
- /// The template for the event required (can contain wildcard).
- /// The (s); where none specified this indicates all.
+ /// The template for the event required (can contain wildcard).
+ /// The (s); where none specified this indicates all.
public EventSubscriberAttribute(string subjectTemplate, params string[] actions)
{
SubjectTemplate = Check.NotEmpty(subjectTemplate, nameof(subjectTemplate));
@@ -23,12 +23,12 @@ public EventSubscriberAttribute(string subjectTemplate, params string[] actions)
}
///
- /// Gets the template for the event required (subscribing to).
+ /// Gets the template for the event required (subscribing to).
///
public string SubjectTemplate { get; private set; }
///
- /// Gets the (s); where none specified this indicates all.
+ /// Gets the (s); where none specified this indicates all.
///
public List Actions { get; private set; }
}
diff --git a/src/Beef.Events/EventSubscriberData.cs b/src/Beef.Events/EventSubscriberData.cs
index 52b7d91d4..8707c39e6 100644
--- a/src/Beef.Events/EventSubscriberData.cs
+++ b/src/Beef.Events/EventSubscriberData.cs
@@ -1,5 +1,7 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+using System;
+
namespace Beef.Events
{
///
@@ -19,9 +21,15 @@ public interface IEventSubscriberData
int Attempt { get; }
///
- /// Gets the .
+ /// Gets or sets the .
///
EventMetadata Metadata { get; }
+
+ ///
+ /// Sets the .
+ ///
+ /// The .
+ void SetMetadata(EventMetadata metadata);
}
///
@@ -54,13 +62,14 @@ public abstract class EventSubscriberData : IEventSubscriberData w
public int Attempt { get; set; }
///
- /// Gets the .
+ /// Gets or sets the .
///
- public EventMetadata Metadata => _metadata ??= GetEventMetadata();
+ public EventMetadata Metadata => _metadata ?? throw new InvalidOperationException("Metadata property must not be accessed prior to being set.");
///
- /// Gets the metadata.
+ /// Sets the .
///
- protected abstract EventMetadata GetEventMetadata();
+ /// The .
+ void IEventSubscriberData.SetMetadata(EventMetadata metadata) => _metadata = Check.NotNull(metadata, nameof(metadata));
}
}
\ No newline at end of file
diff --git a/src/Beef.Events/EventSubscriberHost.cs b/src/Beef.Events/EventSubscriberHost.cs
index 8cba559cb..a1a10bb19 100644
--- a/src/Beef.Events/EventSubscriberHost.cs
+++ b/src/Beef.Events/EventSubscriberHost.cs
@@ -26,7 +26,13 @@ private class EventSubscriberHostInvoker : InvokerBase { }
/// Initializes a new instance of the with the specified .
///
/// The optional .
- public EventSubscriberHost(EventSubscriberHostArgs args) : base(args) { }
+ /// The .
+ public EventSubscriberHost(EventSubscriberHostArgs args, IEventDataConverter eventDataConverter) : base(args) => EventDataConverter = Check.NotNull(eventDataConverter, nameof(eventDataConverter));
+
+ ///
+ /// Gets the .
+ ///
+ public IEventDataConverter EventDataConverter { get; }
///
/// Gets or sets the .
@@ -55,6 +61,23 @@ public THost UseLogger(ILogger logger)
return (THost)this;
}
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ protected override async Task<(EventMetadata? Metadata, Exception? Exception)> GetMetadataAsync(IEventSubscriberData data)
+ {
+ try
+ {
+ return (await EventDataConverter.GetMetadataAsync((TOriginating)data.Originating).ConfigureAwait(false), null);
+ }
+ catch (Exception ex)
+ {
+ return (null, ex);
+ }
+ }
+
///
/// Receives the message and processes.
///
@@ -67,25 +90,19 @@ public Task ReceiveAsync(TData data)
return Invoker.InvokeAsync(this, async () =>
{
// Invoke the base EventSubscriberHost.ReceiveAsync to do the actual work!
- return await ReceiveAsync(data, (subscriber) =>
+ return await ReceiveAsync(data, async (subscriber) =>
{
// Convert/get the beef event data.
try
{
- return GetBeefEventData(data, subscriber);
+ return subscriber.ValueType == null
+ ? await EventDataConverter.ConvertFromAsync(data.Originating).ConfigureAwait(false)
+ : await EventDataConverter.ConvertFromAsync(subscriber.ValueType, data.Originating).ConfigureAwait(false);
}
catch (Exception ex) { throw new EventSubscriberUnhandledException(CreateInvalidEventDataResult(ex)); }
}).ConfigureAwait(false);
}, data);
}
-
- ///
- /// Gets the from the .
- ///
- /// The event/message data.
- /// The identified to process.
- /// The corresponding .
- protected abstract EventData GetBeefEventData(TData data, IEventSubscriber subscriber);
}
///
@@ -147,6 +164,13 @@ protected set
/// This functionality is dependent on the providing the functionality to check and action.
public int? MaxAttempts => Args.MaxAttempts;
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The ; a null indicates that there was a conversion error.
+ protected abstract Task<(EventMetadata? Metadata, Exception? Exception)> GetMetadataAsync(IEventSubscriberData data);
+
///
/// Receives the message and processes when the and has been subscribed.
///
@@ -154,7 +178,7 @@ protected set
/// The function to get the corresponding or only performed where subscribed for processing.
/// The .
/// This method also manages the Dependency Injection (DI) scope for each event execution (see ).
- protected async Task ReceiveAsync(IEventSubscriberData data, Func getEventData)
+ protected async Task ReceiveAsync(IEventSubscriberData data, Func> getEventData)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
@@ -162,6 +186,11 @@ protected async Task ReceiveAsync(IEventSubscriberData data, Func ReceiveAsync(IEventSubscriberData data, Func
/// Checks the and handles accordingly.
///
private async Task CheckResultAsync(IEventSubscriberData data, Result result, IEventSubscriber? subscriber = null)
{
+ if (data == null)
+ throw new ArgumentNullException(nameof(data));
+
if (result == null)
throw new ArgumentNullException(nameof(result));
diff --git a/src/Beef.Events/EventSubscriberHostArgs.cs b/src/Beef.Events/EventSubscriberHostArgs.cs
index 041d0a9bb..ff23ccc28 100644
--- a/src/Beef.Events/EventSubscriberHostArgs.cs
+++ b/src/Beef.Events/EventSubscriberHostArgs.cs
@@ -46,11 +46,11 @@ private static List GetSubscriberConfig(Assembly subscrib
foreach (var type in (subscribersAssembly ?? throw new ArgumentNullException(nameof(subscribersAssembly))).GetTypes().Where(x => typeof(IEventSubscriber).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract))
{
- var esa = type.GetCustomAttribute();
- if (esa == null)
+ var esa = type.GetCustomAttributes();
+ if (esa == null || esa.Count() == 0)
throw new ArgumentException($"Assembly contains Type '{type.Name}' that implements IEventSubscriber but is not decorated with the required EventSubscriberAttribute.", nameof(subscribersAssembly));
- subscribers.Add(new EventSubscriberConfig(esa.SubjectTemplate, esa.Actions, type));
+ esa.ForEach(x => subscribers.Add(new EventSubscriberConfig(x.SubjectTemplate, x.Actions, type)));
}
return subscribers;
@@ -80,11 +80,11 @@ private EventSubscriberHostArgs(params Type[] eventSubscriberTypes)
{
if (typeof(IEventSubscriber).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract)
{
- var esa = type.GetCustomAttribute();
- if (esa == null)
+ var esa = type.GetCustomAttributes();
+ if (esa == null || esa.Count() == 0)
throw new ArgumentException($"Type '{type.Name}' implements IEventSubscriber but is not decorated with the required EventSubscriberAttribute.", nameof(eventSubscriberTypes));
- _subscribers.Add(new EventSubscriberConfig(esa.SubjectTemplate, esa.Actions, type));
+ esa.ForEach(x => _subscribers.Add(new EventSubscriberConfig(x.SubjectTemplate, x.Actions, type)));
}
else
throw new ArgumentException($"Type 'type.name' must implement IEventSubscriber and be decorated with the required EventSubscriberAttribute.");
@@ -295,12 +295,12 @@ public EventSubscriberConfig(string subjectTemplate, List actions, Type
}
///
- /// Gets the template for the event required (subscribing to).
+ /// Gets the template for the event required (subscribing to).
///
public string SubjectTemplate { get; private set; }
///
- /// Gets the (s); where none specified this indicates all.
+ /// Gets the (s); where none specified this indicates all.
///
public List Actions { get; private set; } = new List();
diff --git a/src/Beef.Events/IEventDataContentSerializer.cs b/src/Beef.Events/IEventDataContentSerializer.cs
new file mode 100644
index 000000000..8ace6f5d4
--- /dev/null
+++ b/src/Beef.Events/IEventDataContentSerializer.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Threading.Tasks;
+
+namespace Beef.Events
+{
+ ///
+ /// Enables the serialization of an , as the message content (data), into a corresponding array.
+ ///
+ public interface IEventDataContentSerializer
+ {
+ ///
+ /// Serializes an to a array.
+ ///
+ /// The .
+ /// The array.
+ Task SerializeAsync(EventData @event);
+
+ ///
+ /// Deserializes a array into an .
+ ///
+ /// The array.
+ /// The .
+ Task DeserializeAsync(byte[] bytes);
+
+ ///
+ /// Deserializes a array into an .
+ ///
+ /// The .
+ /// The array.
+ /// The .
+ async Task?> DeserializeAsync(byte[] bytes) => (EventData?)await DeserializeAsync(typeof(T), bytes).ConfigureAwait(false);
+
+ ///
+ /// Deserializes a array into an using the specified .
+ ///
+ /// The .
+ /// The array.
+ /// The .
+ Task DeserializeAsync(Type valueType, byte[] bytes);
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events/IEventDataConverter.cs b/src/Beef.Events/IEventDataConverter.cs
new file mode 100644
index 000000000..fd475060a
--- /dev/null
+++ b/src/Beef.Events/IEventDataConverter.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using System;
+using System.Threading.Tasks;
+
+namespace Beef.Events
+{
+ ///
+ /// Enables the conversion of an into a specific messaging .
+ ///
+ /// The specific messaging .
+ public interface IEventDataConverter where T : class
+ {
+ ///
+ /// Converts an into a of .
+ ///
+ /// The .
+ /// The converted of .
+ Task ConvertToAsync(EventData @event);
+
+ ///
+ /// Converts an of into an .
+ ///
+ /// The event of .
+ /// The converted .
+ Task ConvertFromAsync(T @event);
+
+ ///
+ /// Converts an of into an .
+ ///
+ /// The .
+ /// The event of .
+ /// The converted .
+ async Task> ConvertFromAsync(T @event) => (EventData) await ConvertFromAsync(typeof(TEventData), @event).ConfigureAwait(false);
+
+ ///
+ /// Converts an of into an .
+ ///
+ /// The .
+ /// The event of .
+ /// The converted .
+ Task ConvertFromAsync(Type valueType, T @event);
+
+ ///
+ /// Gets the from the of .
+ ///
+ /// The event of .
+ /// The corresponding .
+ Task GetMetadataAsync(T @event);
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events/NewtonsoftJsonCloudEventSerializer.cs b/src/Beef.Events/NewtonsoftJsonCloudEventSerializer.cs
new file mode 100644
index 000000000..82b9cae52
--- /dev/null
+++ b/src/Beef.Events/NewtonsoftJsonCloudEventSerializer.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using CloudNative.CloudEvents;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Linq;
+using System.Net.Mime;
+using System.Threading.Tasks;
+
+namespace Beef.Events
+{
+ ///
+ /// Provides a using the Newtonsoft.
+ ///
+ public class NewtonsoftJsonCloudEventSerializer : IEventDataContentSerializer
+ {
+ private const string _beefJsonName = "beef";
+
+ ///
+ /// Gets or sets the JSON extension object name. Defaults to `beef`;
+ ///
+ public string EventMetadataExtensionName { get; set; } = _beefJsonName;
+
+ ///
+ /// Indicates whether to include the as attributes.
+ ///
+ /// Defaults to true.
+ public bool IncludeEventMetadata { get; set; } = true;
+
+ ///
+ /// Gets or sets the list of property names that are serialized/deserialized when is true.
+ ///
+ /// Defaults to: , , , ,
+ /// and . An empty array indicates that all are included.
+ public string[] IncludeEventMetadataProperties { get; set; } =
+ new string[] { nameof(EventMetadata.Subject), nameof(EventMetadata.Action), nameof(EventMetadata.TenantId), nameof(EventMetadata.UserId), nameof(EventMetadata.Username), nameof(EventMetadata.CorrelationId) };
+
+ ///
+ /// Gets or sets the default where the is not specified.
+ ///
+ /// Defaults to '/notspecified'.
+ public Uri DefaultSource { get; set; } = new Uri("/notspecified", UriKind.Relative);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task SerializeAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (string.IsNullOrEmpty(@event.Subject))
+ throw new ArgumentException("Subject must be specified.", nameof(@event));
+
+ var type = $"{@event.Subject}{(string.IsNullOrEmpty(@event.Action) ? "" : $".{@event.Action}")}";
+
+ var ce = new CloudEvent(type, @event.Source ?? DefaultSource, @event.EventId?.ToString(), @event.Timestamp);
+ if (@event.HasValue)
+ {
+ ce.DataContentType = new ContentType(MediaTypeNames.Application.Json);
+ ce.Data = @event.GetValue();
+ };
+
+ if (IncludeEventMetadata)
+ {
+ // Add metadata (remove values which are already part of the payload).
+ var md = @event.CopyMetadata();
+ SetOrNullMetadata(md);
+
+ // Add metadata as an attribute.
+ var ces = ce.GetAttributes();
+ ces.Add(EventMetadataExtensionName ??= _beefJsonName, md);
+ }
+
+ return Task.FromResult(new JsonEventFormatter().EncodeStructuredEvent(ce, out var _));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task DeserializeAsync(byte[] bytes)
+ {
+ var d = Deserialize(bytes);
+ if (d.CloudEvent == null)
+ return Task.FromResult(null);
+
+ return Task.FromResult(new EventData(d.Metadata));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task DeserializeAsync(Type valueType, byte[] bytes)
+ {
+ var d = Deserialize(bytes);
+ if (d.CloudEvent?.Data == null)
+ return Task.FromResult(null);
+
+ if (d.CloudEvent.DataContentType.MediaType != MediaTypeNames.Application.Json)
+ throw new InvalidOperationException($"CloudEvent DataContentType.MediaType is '{d.CloudEvent.DataContentType.Name}', it must be '{MediaTypeNames.Application.Json}' to use the '{nameof(NewtonsoftJsonCloudEventSerializer)}'.");
+
+ var ed = (EventData)Activator.CreateInstance(NewtonsoftJsonEventDataSerializer.CreateValueEventDataType(valueType), new object[] { d.Metadata! });
+ ed.SetValue(d.CloudEvent.Data is JToken json ? json.ToObject(valueType) : Convert.ChangeType(d.CloudEvent.Data, valueType));
+ return Task.FromResult(ed);
+ }
+
+ ///
+ /// Perform the common deserialization.
+ ///
+ private (CloudEvent? CloudEvent, EventMetadata? Metadata) Deserialize(byte[] bytes)
+ {
+ if (bytes == null || bytes.Length == 0)
+ return (null, null);
+
+ var ce = new JsonEventFormatter().DecodeStructuredEvent(bytes);
+
+ EventMetadata? md = null;
+ if (IncludeEventMetadata && ce.GetAttributes().TryGetValue(EventMetadataExtensionName ??= _beefJsonName, out object val) && val != null && val is JToken json)
+ {
+ md = json.ToObject();
+ SetOrNullMetadata(md);
+ }
+
+ if (md == null)
+ md = new EventMetadata();
+
+ if (string.IsNullOrEmpty(md.Subject))
+ md.Subject = ce.Type;
+
+ md.Source = ce.Source;
+ md.EventId = string.IsNullOrEmpty(ce.Id) ? null : (Guid.TryParse(ce.Id, out var eid) ? eid : (Guid?)null);
+ md.Timestamp = ce.Time;
+
+ return (ce, md);
+ }
+
+ ///
+ /// Sets or nulls the metadata based on the name inclusion.
+ ///
+ private void SetOrNullMetadata(EventMetadata? md)
+ {
+ if (md == null)
+ return;
+
+ md.EventId = SetOrNullValue(nameof(EventMetadata.EventId), md.EventId);
+ md.TenantId = SetOrNullValue(nameof(EventMetadata.TenantId), md.TenantId);
+ md.Subject = SetOrNullValue(nameof(EventMetadata.Subject), md.Subject);
+ md.Action = SetOrNullValue(nameof(EventMetadata.Action), md.Action);
+ md.Source = SetOrNullValue(nameof(EventMetadata.Source), md.Source);
+ md.Key = SetOrNullValue(nameof(EventMetadata.Key), md.Key);
+ md.Username = SetOrNullValue(nameof(EventMetadata.Username), md.Username);
+ md.UserId = SetOrNullValue(nameof(EventMetadata.UserId), md.UserId);
+ md.Timestamp = SetOrNullValue(nameof(EventMetadata.Timestamp), md.Timestamp);
+ md.CorrelationId = SetOrNullValue(nameof(EventMetadata.CorrelationId), md.CorrelationId);
+ md.ETag = SetOrNullValue(nameof(EventMetadata.ETag), md.ETag);
+ md.PartitionKey = SetOrNullValue(nameof(EventMetadata.PartitionKey), md.PartitionKey);
+ }
+
+ ///
+ /// Set the value of null depending on configuration.
+ ///
+ private T SetOrNullValue(string name, T value) =>
+ IncludeEventMetadataProperties == null || IncludeEventMetadataProperties.Length == 0 || IncludeEventMetadataProperties.Any(x => string.Equals(x, name, StringComparison.InvariantCultureIgnoreCase)) ? value : default!;
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events/NewtonsoftJsonEventDataSerializer.cs b/src/Beef.Events/NewtonsoftJsonEventDataSerializer.cs
new file mode 100644
index 000000000..56aaa5a48
--- /dev/null
+++ b/src/Beef.Events/NewtonsoftJsonEventDataSerializer.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
+
+using Newtonsoft.Json;
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beef.Events
+{
+ ///
+ /// Provides an using the Newtonsoft.
+ ///
+ public class NewtonsoftJsonEventDataSerializer : IEventDataContentSerializer
+ {
+ private readonly JsonSerializer _jsonSerializer;
+
+ ///
+ /// Initializes a new instance of the class using a default .
+ ///
+ public NewtonsoftJsonEventDataSerializer() => _jsonSerializer = JsonSerializer.CreateDefault();
+
+ ///
+ /// Initializes a new instance of the class using the specified .
+ ///
+ /// The .
+ public NewtonsoftJsonEventDataSerializer(JsonSerializer jsonSerializer) => _jsonSerializer = Check.NotNull(jsonSerializer, nameof(jsonSerializer));
+
+ ///
+ /// Indicates whether the is serialized only; or alternatively, the complete (default).
+ ///
+ public bool SerializeValueOnly { get; set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task SerializeAsync(EventData @event)
+ {
+ if (@event == null)
+ throw new ArgumentNullException(nameof(@event));
+
+ if (SerializeValueOnly && !@event.HasValue)
+ return Task.FromResult(Array.Empty());
+
+ using var stream = new MemoryStream();
+ using (var writer = new StreamWriter(stream, Encoding.UTF8))
+ {
+ if (SerializeValueOnly)
+ _jsonSerializer.Serialize(writer, @event.GetValue());
+ else
+ _jsonSerializer.Serialize(writer, @event);
+ }
+
+ return Task.FromResult(stream.ToArray());
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task DeserializeAsync(byte[] bytes)
+ {
+ if (SerializeValueOnly)
+ return Task.FromResult(new EventData(null));
+
+ if (bytes == null || bytes.Length == 0)
+ return Task.FromResult(null);
+
+ using var stream = new MemoryStream(bytes);
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+ return Task.FromResult((EventData?)_jsonSerializer.Deserialize(reader, typeof(EventData)));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Task DeserializeAsync(Type valueType, byte[] bytes)
+ {
+ if (bytes == null || bytes.Length == 0)
+ return Task.FromResult(null);
+
+ using var stream = new MemoryStream(bytes);
+ using var reader = new StreamReader(stream, Encoding.UTF8);
+
+ if (SerializeValueOnly)
+ {
+ var val = _jsonSerializer.Deserialize(reader, valueType);
+ var ed = (EventData)Activator.CreateInstance(CreateValueEventDataType(valueType), new object[] { (EventMetadata)null! });
+ if (val != null)
+ ed.SetValue(val);
+
+ return Task.FromResult(ed);
+ }
+
+ return Task.FromResult((EventData?)_jsonSerializer.Deserialize(reader, CreateValueEventDataType(valueType)));
+ }
+
+ ///
+ /// Create the .
+ ///
+ /// The .
+ /// The corresponding .
+ internal static Type CreateValueEventDataType(Type valueType) => typeof(EventData<>).MakeGenericType(Check.NotNull(valueType, nameof(valueType)));
+ }
+}
\ No newline at end of file
diff --git a/src/Beef.Events/RunAsUser.cs b/src/Beef.Events/RunAsUser.cs
index 399ac5f21..df1668ee7 100644
--- a/src/Beef.Events/RunAsUser.cs
+++ b/src/Beef.Events/RunAsUser.cs
@@ -8,7 +8,7 @@ namespace Beef.Events
public enum RunAsUser
{
///
- /// Run as the originating user (see ) for the message.
+ /// Run as the originating user (see ) for the message.
///
Originating,
diff --git a/templates/Beef.Template.Solution/Beef.Template.Solution.csproj b/templates/Beef.Template.Solution/Beef.Template.Solution.csproj
index 52ba6670b..bf37954b8 100644
--- a/templates/Beef.Template.Solution/Beef.Template.Solution.csproj
+++ b/templates/Beef.Template.Solution/Beef.Template.Solution.csproj
@@ -2,7 +2,7 @@
netcoreapp3.1
- 4.1.6
+ 4.1.7Beef DevelopersAvanadeBusiness Entity Execution Framework (Beef) template solution for use with 'dotnet new'.
diff --git a/templates/Beef.Template.Solution/CHANGELOG.md b/templates/Beef.Template.Solution/CHANGELOG.md
index a0a97db95..7916011d7 100644
--- a/templates/Beef.Template.Solution/CHANGELOG.md
+++ b/templates/Beef.Template.Solution/CHANGELOG.md
@@ -2,6 +2,9 @@
Represents the **NuGet** versions.
+## v4.1.7
+- *Fixed:* Updated referenced *Beef* NuGet references to latest.
+
## v4.1.6
- *Fixed:* Updated referenced *Beef* NuGet references to latest.
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Api/Company.AppName.Api.csproj b/templates/Beef.Template.Solution/content/Company.AppName.Api/Company.AppName.Api.csproj
index 5833979a9..e4e564713 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.Api/Company.AppName.Api.csproj
+++ b/templates/Beef.Template.Solution/content/Company.AppName.Api/Company.AppName.Api.csproj
@@ -11,8 +11,8 @@
-
-
+
+
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs b/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs
index e8c2a57e8..9b8c3f4a3 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs
+++ b/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Reflection;
+using Azure.Messaging.EventHubs.Producer;
using Beef;
using Beef.AspNetCore.WebApi;
using Beef.Caching.Policy;
@@ -96,7 +97,7 @@ public void ConfigureServices(IServiceCollection services)
// Add event publishing services.
var ehcs = _config.GetValue("EventHubConnectionString");
if (!string.IsNullOrEmpty(ehcs))
- services.AddBeefEventHubEventProducer(ehcs);
+ services.AddBeefEventHubEventProducer(new EventHubProducerClient(ehcs));
else
services.AddBeefNullEventPublisher();
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/Company.AppName.CodeGen.csproj b/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/Company.AppName.CodeGen.csproj
index f92c1a72f..d28554b6d 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/Company.AppName.CodeGen.csproj
+++ b/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/Company.AppName.CodeGen.csproj
@@ -5,6 +5,6 @@
enable
-
+
\ No newline at end of file
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Common/Company.AppName.Common.csproj b/templates/Beef.Template.Solution/content/Company.AppName.Common/Company.AppName.Common.csproj
index e8f5d9d54..aa2a23038 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.Common/Company.AppName.Common.csproj
+++ b/templates/Beef.Template.Solution/content/Company.AppName.Common/Company.AppName.Common.csproj
@@ -4,7 +4,7 @@
enable
-
+
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Database/Company.AppName.Database.csproj b/templates/Beef.Template.Solution/content/Company.AppName.Database/Company.AppName.Database.csproj
index 29cb05025..faa2bed4e 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.Database/Company.AppName.Database.csproj
+++ b/templates/Beef.Template.Solution/content/Company.AppName.Database/Company.AppName.Database.csproj
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Test/Company.AppName.Test.csproj b/templates/Beef.Template.Solution/content/Company.AppName.Test/Company.AppName.Test.csproj
index b0654ea2d..3795ae8c2 100644
--- a/templates/Beef.Template.Solution/content/Company.AppName.Test/Company.AppName.Test.csproj
+++ b/templates/Beef.Template.Solution/content/Company.AppName.Test/Company.AppName.Test.csproj
@@ -18,7 +18,7 @@
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Beef.Events.UnitTest/Beef.Events.UnitTest.csproj b/tests/Beef.Events.UnitTest/Beef.Events.UnitTest.csproj
index e640592b6..35511d618 100644
--- a/tests/Beef.Events.UnitTest/Beef.Events.UnitTest.csproj
+++ b/tests/Beef.Events.UnitTest/Beef.Events.UnitTest.csproj
@@ -25,6 +25,7 @@
+
diff --git a/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonCloudEventSerializerTest.cs b/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonCloudEventSerializerTest.cs
new file mode 100644
index 000000000..cb91a88b1
--- /dev/null
+++ b/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonCloudEventSerializerTest.cs
@@ -0,0 +1,166 @@
+using Newtonsoft.Json;
+using NUnit.Framework;
+using System;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beef.Events.UnitTest.ContentSerializers
+{
+ [TestFixture]
+ public class NewtonsoftJsonCloudEventSerializerTest
+ {
+ [JsonObject(MemberSerialization = MemberSerialization.OptIn)]
+ public class Person
+ {
+ [JsonProperty("first")]
+ public string FirstName;
+ [JsonProperty("last")]
+ public string LastName;
+
+ public string Private;
+
+ public static Person Create() => new Person { FirstName = "Rebecca", LastName = "Brown", Private = "Top secret" };
+ }
+
+ [Test]
+ public async Task EventDataEndToEndWithBeef()
+ {
+ var eds = new NewtonsoftJsonCloudEventSerializer { IncludeEventMetadataProperties = null };
+ var bytes = await eds.SerializeAsync(new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata()));
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual(@"{
+ ""specversion"": ""1.0"",
+ ""type"": ""Test.Subject.Created"",
+ ""source"": ""/test"",
+ ""id"": ""00000001-0000-0000-0000-000000000000"",
+ ""time"": ""2001-01-15T12:48:16"",
+ ""beef"": {
+ ""eventId"": ""00000001-0000-0000-0000-000000000000"",
+ ""tenantId"": ""00000002-0000-0000-0000-000000000000"",
+ ""subject"": ""Test.Subject"",
+ ""action"": ""Created"",
+ ""source"": ""/test"",
+ ""key"": 1,
+ ""username"": ""Bob"",
+ ""userid"": ""123"",
+ ""timestamp"": ""2001-01-15T12:48:16"",
+ ""correlationId"": ""XXX"",
+ ""etag"": ""YYY"",
+ ""partitionKey"": ""PK""
+ }
+}", json);
+
+ var ed = await eds.DeserializeAsync(bytes);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(ed);
+ }
+
+ [Test]
+ public async Task EventDataTEndToEndWithBeef()
+ {
+ var eds = new NewtonsoftJsonCloudEventSerializer { IncludeEventMetadataProperties = null };
+ var bytes = await eds.SerializeAsync(new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata()) { Value = Person.Create() });
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual(@"{
+ ""specversion"": ""1.0"",
+ ""type"": ""Test.Subject.Created"",
+ ""source"": ""/test"",
+ ""id"": ""00000001-0000-0000-0000-000000000000"",
+ ""time"": ""2001-01-15T12:48:16"",
+ ""datacontenttype"": ""application/json"",
+ ""data"": {
+ ""first"": ""Rebecca"",
+ ""last"": ""Brown""
+ },
+ ""beef"": {
+ ""eventId"": ""00000001-0000-0000-0000-000000000000"",
+ ""tenantId"": ""00000002-0000-0000-0000-000000000000"",
+ ""subject"": ""Test.Subject"",
+ ""action"": ""Created"",
+ ""source"": ""/test"",
+ ""key"": 1,
+ ""username"": ""Bob"",
+ ""userid"": ""123"",
+ ""timestamp"": ""2001-01-15T12:48:16"",
+ ""correlationId"": ""XXX"",
+ ""etag"": ""YYY"",
+ ""partitionKey"": ""PK""
+ }
+}", json);
+
+ var ed = await eds.DeserializeAsync(typeof(Person), bytes);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(ed);
+ Assert.NotNull(ed.GetValue());
+
+ var p = ((EventData)ed).Value;
+ Assert.NotNull(p);
+ Assert.AreEqual("Rebecca", p.FirstName);
+ Assert.AreEqual("Brown", p.LastName);
+ Assert.Null(p.Private);
+ }
+
+ [Test]
+ public async Task EventDataEndToEndNoBeef()
+ {
+ var eds = new NewtonsoftJsonCloudEventSerializer { IncludeEventMetadata = false };
+ var bytes = await eds.SerializeAsync(new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata()));
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual(@"{
+ ""specversion"": ""1.0"",
+ ""type"": ""Test.Subject.Created"",
+ ""source"": ""/test"",
+ ""id"": ""00000001-0000-0000-0000-000000000000"",
+ ""time"": ""2001-01-15T12:48:16""
+}", json);
+
+ var ed = await eds.DeserializeAsync(bytes);
+ AssertPartialEventMetadata(ed);
+ }
+
+ [Test]
+ public async Task EventDataTEndToEndNoBeef()
+ {
+ var eds = new NewtonsoftJsonCloudEventSerializer { IncludeEventMetadata = false };
+ var bytes = await eds.SerializeAsync(new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata()) { Value = 88 });
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual(@"{
+ ""specversion"": ""1.0"",
+ ""type"": ""Test.Subject.Created"",
+ ""source"": ""/test"",
+ ""id"": ""00000001-0000-0000-0000-000000000000"",
+ ""time"": ""2001-01-15T12:48:16"",
+ ""datacontenttype"": ""application/json"",
+ ""data"": 88
+}", json);
+
+ var ed = await eds.DeserializeAsync(typeof(int), bytes);
+ AssertPartialEventMetadata(ed);
+ Assert.AreEqual(88, ed.GetValue());
+ Assert.AreEqual(88, ((EventData)ed).Value);
+ }
+
+ public static void AssertPartialEventMetadata(EventMetadata metadata)
+ {
+ Assert.IsNotNull(metadata);
+ Assert.AreEqual("Test.Subject.Created", metadata.Subject);
+ Assert.AreEqual(new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), metadata.EventId);
+ Assert.AreEqual(new Uri("/test", UriKind.Relative), metadata.Source);
+ Assert.AreEqual(new DateTime(2001, 01, 15, 12, 48, 16), metadata.Timestamp);
+ Assert.Null(metadata.Action);
+ Assert.Null(metadata.CorrelationId);
+ Assert.Null(metadata.TenantId);
+ Assert.Null(metadata.ETag);
+ Assert.Null(metadata.Key);
+ Assert.Null(metadata.PartitionKey);
+ Assert.Null(metadata.UserId);
+ Assert.Null(metadata.Username);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonEventDataSerializerTest.cs b/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonEventDataSerializerTest.cs
new file mode 100644
index 000000000..2fb529f84
--- /dev/null
+++ b/tests/Beef.Events.UnitTest/ContentSerializers/NewtonsoftJsonEventDataSerializerTest.cs
@@ -0,0 +1,121 @@
+using NUnit.Framework;
+using System;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Beef.Events.UnitTest.ContentSerializers
+{
+ [TestFixture]
+ public class NewtonsoftJsonEventDataSerializerTest
+ {
+ [Test]
+ public async Task EventDataEndToEnd()
+ {
+ var eds = new NewtonsoftJsonEventDataSerializer();
+ var bytes = await eds.SerializeAsync(new EventData(CreateEventMetadata()));
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual("{\"eventId\":\"00000001-0000-0000-0000-000000000000\",\"tenantId\":\"00000002-0000-0000-0000-000000000000\",\"subject\":\"Test.Subject\",\"action\":\"Created\",\"source\":\"/test\",\"key\":1,\"username\":\"Bob\",\"userid\":\"123\",\"timestamp\":\"2001-01-15T12:48:16\",\"correlationId\":\"XXX\",\"etag\":\"YYY\",\"partitionKey\":\"PK\"}", json);
+
+ var ed = await eds.DeserializeAsync(bytes);
+ AssertEventMetadata(ed);
+ }
+
+ [Test]
+ public async Task EventDataTEndToEnd()
+ {
+ var eds = new NewtonsoftJsonEventDataSerializer();
+ var bytes = await eds.SerializeAsync(new EventData(CreateEventMetadata()) { Value = 88 });
+ Assert.Greater(bytes.Length, 0);
+
+ var json = Encoding.UTF8.GetString(bytes);
+ Assert.AreEqual("{\"value\":88,\"eventId\":\"00000001-0000-0000-0000-000000000000\",\"tenantId\":\"00000002-0000-0000-0000-000000000000\",\"subject\":\"Test.Subject\",\"action\":\"Created\",\"source\":\"/test\",\"key\":1,\"username\":\"Bob\",\"userid\":\"123\",\"timestamp\":\"2001-01-15T12:48:16\",\"correlationId\":\"XXX\",\"etag\":\"YYY\",\"partitionKey\":\"PK\"}", json);
+
+ var ed = await eds.DeserializeAsync(typeof(int), bytes);
+ AssertEventMetadata(ed);
+ Assert.AreEqual(88, ed.GetValue());
+ Assert.AreEqual(88, ((EventData)ed).Value);
+ }
+
+ [Test]
+ public async Task EventDataValueOnly()
+ {
+ var eds = new NewtonsoftJsonEventDataSerializer { SerializeValueOnly = true };
+ var bytes = await eds.SerializeAsync(new EventData(CreateEventMetadata()));
+ Assert.AreEqual(bytes.Length, 0);
+
+ var ed = await eds.DeserializeAsync(bytes);
+ Assert.NotNull(ed);
+ Assert.Null(ed.Subject);
+ Assert.IsFalse(ed.HasValue);
+ }
+
+ [Test]
+ public async Task EventDataTValueOnly()
+ {
+ var eds = new NewtonsoftJsonEventDataSerializer { SerializeValueOnly = true };
+ var bytes = await eds.SerializeAsync(new EventData(CreateEventMetadata()) { Value = 88 });
+ Assert.Greater(bytes.Length, 0);
+
+ var ed = await eds.DeserializeAsync(typeof(int), bytes);
+ Assert.NotNull(ed);
+ Assert.Null(ed.Subject);
+ Assert.IsTrue(ed.HasValue);
+ Assert.AreEqual(88, ed.GetValue());
+ Assert.AreEqual(88, ((EventData)ed).Value);
+ }
+
+ public static EventMetadata CreateEventMetadata()
+ {
+ return new EventMetadata
+ {
+ Subject = "Test.Subject",
+ Action = "Created",
+ EventId = new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ CorrelationId = "XXX",
+ Source = new Uri("/test", UriKind.Relative),
+ TenantId = new Guid(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+ Timestamp = new DateTime(2001, 01, 15, 12, 48, 16),
+ ETag = "YYY",
+ Key = 1,
+ PartitionKey = "PK",
+ UserId = "123",
+ Username = "Bob"
+ };
+ }
+
+ public static void AssertEventMetadata(EventMetadata metadata, bool defaultPropertiesOnly = false)
+ {
+ Assert.IsNotNull(metadata);
+ Assert.AreEqual("Test.Subject", metadata.Subject);
+ Assert.AreEqual("Created", metadata.Action);
+ if (defaultPropertiesOnly)
+ {
+ Assert.AreEqual(new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), metadata.EventId);
+ Assert.AreEqual("XXX", metadata.CorrelationId);
+ Assert.AreEqual(new Uri("/test", UriKind.Relative), metadata.Source);
+ Assert.AreEqual(new Guid(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), metadata.TenantId);
+ Assert.AreEqual(new DateTime(2001, 01, 15, 12, 48, 16), metadata.Timestamp);
+ Assert.Null(metadata.ETag);
+ Assert.Null(metadata.Key);
+ Assert.Null(metadata.PartitionKey);
+ Assert.AreEqual("123", metadata.UserId);
+ Assert.AreEqual("Bob", metadata.Username);
+ }
+ else
+ {
+ Assert.AreEqual(new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), metadata.EventId);
+ Assert.AreEqual("XXX", metadata.CorrelationId);
+ Assert.AreEqual(new Uri("/test", UriKind.Relative), metadata.Source);
+ Assert.AreEqual(new Guid(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), metadata.TenantId);
+ Assert.AreEqual(new DateTime(2001, 01, 15, 12, 48, 16), metadata.Timestamp);
+ Assert.AreEqual("YYY", metadata.ETag);
+ Assert.AreEqual(1, metadata.Key);
+ Assert.AreEqual("PK", metadata.PartitionKey);
+ Assert.AreEqual("123", metadata.UserId);
+ Assert.AreEqual("Bob", metadata.Username);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Beef.Events.UnitTest/Converters/EventDataConverterTester.cs b/tests/Beef.Events.UnitTest/Converters/EventDataConverterTester.cs
new file mode 100644
index 000000000..4f7f8f402
--- /dev/null
+++ b/tests/Beef.Events.UnitTest/Converters/EventDataConverterTester.cs
@@ -0,0 +1,133 @@
+using Beef.Events.EventHubs;
+using Beef.Events.ServiceBus;
+using Beef.Events.UnitTest.ContentSerializers;
+using NUnit.Framework;
+using System;
+using System.Threading.Tasks;
+using AzureEventHubs = Azure.Messaging.EventHubs;
+using MicrosoftEventHubs = Microsoft.Azure.EventHubs;
+using AzureServiceBus = Azure.Messaging.ServiceBus;
+using MicrosoftServiceBus = Microsoft.Azure.ServiceBus;
+
+namespace Beef.Events.UnitTest.Converters
+{
+ [TestFixture]
+ public partial class AzureEventHubsEventConverterTest
+ {
+ [Test]
+ public Task EndToEndWithMessagingProperties() => new EventDataConverterTester().EndToEnd(new AzureEventHubsEventConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.Properties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndValueWithMessagingProperties() => new EventDataConverterTester().EndToEndValue(new AzureEventHubsEventConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.Properties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndWithoutMessagingProperties() => new EventDataConverterTester().EndToEnd(new AzureEventHubsEventConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.Properties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+
+ [Test]
+ public Task EndToEndValueWithoutMessagingProperties() => new EventDataConverterTester().EndToEndValue(new AzureEventHubsEventConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.Properties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+ }
+
+ [TestFixture]
+ public class MicrosoftEventHubsEventConverterTest
+ {
+ [Test]
+ public Task EndToEndWithMessagingProperties() => new EventDataConverterTester().EndToEnd(new MicrosoftEventHubsEventConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.Properties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndValueWithMessagingProperties() => new EventDataConverterTester().EndToEndValue(new MicrosoftEventHubsEventConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.Properties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndWithoutMessagingProperties() => new EventDataConverterTester().EndToEnd(new MicrosoftEventHubsEventConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.Properties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+
+ [Test]
+ public Task EndToEndValueWithoutMessagingProperties() => new EventDataConverterTester().EndToEndValue(new MicrosoftEventHubsEventConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.Properties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+ }
+
+ [TestFixture]
+ public class AzureServiceBusMessageConverterTest
+ {
+ [Test]
+ public Task EndToEndWithMessagingProperties() => new EventDataConverterTester().EndToEnd(new AzureServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.ApplicationProperties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndValueWithMessagingProperties() => new EventDataConverterTester().EndToEndValue(new AzureServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.ApplicationProperties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndWithoutMessagingProperties() => new EventDataConverterTester().EndToEnd(new AzureServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.ApplicationProperties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+
+ [Test]
+ public Task EndToEndValueWithoutMessagingProperties() => new EventDataConverterTester().EndToEndValue(new AzureServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.ApplicationProperties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+ }
+
+ [TestFixture]
+ public class MicrosoftServiceBusMessageConverterTest
+ {
+ [Test]
+ public Task EndToEndWithMessagingProperties() => new EventDataConverterTester().EndToEnd(new MicrosoftServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.UserProperties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndValueWithMessagingProperties() => new EventDataConverterTester().EndToEndValue(new MicrosoftServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = true },
+ ce => Assert.AreEqual("Test.Subject", ce.UserProperties[EventMetadata.SubjectAttributeName]), false);
+
+ [Test]
+ public Task EndToEndWithoutMessagingProperties() => new EventDataConverterTester().EndToEnd(new MicrosoftServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.UserProperties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+
+ [Test]
+ public Task EndToEndValueWithoutMessagingProperties() => new EventDataConverterTester().EndToEndValue(new MicrosoftServiceBusMessageConverter() { UseMessagingPropertiesForMetadata = false },
+ ce => Assert.False(ce.UserProperties.ContainsKey(EventMetadata.SubjectAttributeName)), true);
+ }
+
+ public class EventDataConverterTester where T : IEventDataConverter where CT : class
+ {
+ public async Task EndToEnd(T edc, Action action, bool defaultPropertiesOnly)
+ {
+ var ed = new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata());
+ var ce = await edc.ConvertToAsync(ed);
+ Assert.NotNull(ce);
+
+ action(ce);
+
+ var md = await edc.GetMetadataAsync(ce);
+ Assert.NotNull(md);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(md, defaultPropertiesOnly);
+
+ var ed2 = await edc.ConvertFromAsync(ce);
+ Assert.NotNull(ed2);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(ed2, defaultPropertiesOnly);
+ }
+
+ public async Task EndToEndValue(T edc, Action action, bool defaultPropertiesOnly)
+ {
+ var ed = new EventData(NewtonsoftJsonEventDataSerializerTest.CreateEventMetadata()) { Value = 88 };
+ var ce = await edc.ConvertToAsync(ed);
+ Assert.NotNull(ce);
+
+ action(ce);
+
+ var md = await edc.GetMetadataAsync(ce);
+ Assert.NotNull(md);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(md, defaultPropertiesOnly);
+
+ var ed2 = await edc.ConvertFromAsync(typeof(int), ce);
+ Assert.NotNull(ed2);
+ NewtonsoftJsonEventDataSerializerTest.AssertEventMetadata(ed2, defaultPropertiesOnly);
+ Assert.True(ed2.HasValue);
+ Assert.AreEqual(88, ed2.GetValue());
+ Assert.AreEqual(88, ((EventData)ed2).Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Beef.Events.UnitTest/EventDataMapperTest.cs b/tests/Beef.Events.UnitTest/Converters/EventDataMapperTest.cs
similarity index 56%
rename from tests/Beef.Events.UnitTest/EventDataMapperTest.cs
rename to tests/Beef.Events.UnitTest/Converters/EventDataMapperTest.cs
index 63cec2a88..60e03b616 100644
--- a/tests/Beef.Events.UnitTest/EventDataMapperTest.cs
+++ b/tests/Beef.Events.UnitTest/Converters/EventDataMapperTest.cs
@@ -1,26 +1,25 @@
using Beef.Entities;
using NUnit.Framework;
using System;
-using System.Collections.Generic;
-using System.Text;
+using System.Threading.Tasks;
-namespace Beef.Events.UnitTest
+namespace Beef.Events.UnitTest.Converters
{
[TestFixture]
- public class EventDataMapperTest
+ public partial class AzureEventHubsEventConverterTest
{
[Test]
- public void SubjectOnly()
+ public async Task SubjectOnly()
{
var ed = EventData.CreateEvent("Subject");
- var eh = ed.ToEventHubsEventData();
+ var eh = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertToAsync(ed);
Assert.IsNotNull(eh);
- Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.ActionPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.TenantIdPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.KeyPropertyName]);
+ Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectAttributeName]);
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.ActionAttributeName));
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.TenantIdAttributeName));
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.KeyPropertyName));
- ed = eh.ToBeefEventData();
+ ed = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertFromAsync(eh);
Assert.IsNotNull(eh);
Assert.AreEqual("Subject", ed.Subject);
Assert.AreEqual(null, ed.Action);
@@ -29,17 +28,17 @@ public void SubjectOnly()
}
[Test]
- public void SubjectAndAction()
+ public async Task SubjectAndAction()
{
var ed = EventData.CreateEvent("Subject", "Action");
- var eh = ed.ToEventHubsEventData();
+ var eh = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertToAsync(ed);
Assert.IsNotNull(eh);
- Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectPropertyName]);
- Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.TenantIdPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.KeyPropertyName]);
+ Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectAttributeName]);
+ Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionAttributeName]);
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.TenantIdAttributeName));
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.KeyPropertyName));
- ed = eh.ToBeefEventData();
+ ed = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertFromAsync(eh);
Assert.IsNotNull(eh);
Assert.AreEqual("Subject", ed.Subject);
Assert.AreEqual("Action", ed.Action);
@@ -48,22 +47,22 @@ public void SubjectAndAction()
}
[Test]
- public void SubjectActionAndKey()
+ public async Task SubjectActionAndKey()
{
var id = Guid.NewGuid();
var ed = EventData.CreateEvent("Subject", "Action", id);
Assert.IsNotNull(ed.EventId);
var eid = ed.EventId;
- var eh = ed.ToEventHubsEventData();
+ var eh = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertToAsync(ed);
Assert.IsNotNull(eh);
- Assert.AreEqual(eid, eh.Properties[EventMetadata.EventIdPropertyName]);
- Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectPropertyName]);
- Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.TenantIdPropertyName]);
+ Assert.AreEqual(eid, eh.Properties[EventMetadata.EventIdAttributeName]);
+ Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectAttributeName]);
+ Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionAttributeName]);
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.TenantIdAttributeName));
Assert.AreEqual(id, eh.Properties[EventMetadata.KeyPropertyName]);
- ed = eh.ToBeefEventData();
+ ed = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertFromAsync(eh);
Assert.IsNotNull(eh);
Assert.AreEqual(eid, ed.EventId);
Assert.AreEqual("Subject", ed.Subject);
@@ -73,21 +72,21 @@ public void SubjectActionAndKey()
}
[Test]
- public void SubjectActionAndArrayKey()
+ public async Task SubjectActionAndArrayKey()
{
var id = Guid.NewGuid();
var no = 123;
var ed = EventData.CreateEvent("Subject", "Action", id, no);
- var eh = ed.ToEventHubsEventData();
+ var eh = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertToAsync(ed);
Assert.IsNotNull(eh);
- Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectPropertyName]);
- Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.TenantIdPropertyName]);
+ Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectAttributeName]);
+ Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionAttributeName]);
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.TenantIdAttributeName));
Assert.AreEqual(id, ((object[])eh.Properties[EventMetadata.KeyPropertyName])[0]);
Assert.AreEqual(no, ((object[])eh.Properties[EventMetadata.KeyPropertyName])[1]);
- ed = eh.ToBeefEventData();
+ ed = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertFromAsync(eh);
Assert.IsNotNull(eh);
Assert.AreEqual("Subject", ed.Subject);
Assert.AreEqual("Action", ed.Action);
@@ -103,19 +102,19 @@ public class Person : IGuidIdentifier
}
[Test]
- public void SubjectActionKeyAndValue()
+ public async Task SubjectActionKeyAndValue()
{
var p = new Person { Id = Guid.NewGuid(), Name = "Caleb" };
var ed = EventData.CreateValueEvent(p, "Subject", "Action");
- var eh = ed.ToEventHubsEventData();
+ var eh = await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertToAsync(ed);
Assert.IsNotNull(eh);
- Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectPropertyName]);
- Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionPropertyName]);
- Assert.AreEqual(null, eh.Properties[EventMetadata.TenantIdPropertyName]);
+ Assert.AreEqual("Subject", eh.Properties[EventMetadata.SubjectAttributeName]);
+ Assert.AreEqual("Action", eh.Properties[EventMetadata.ActionAttributeName]);
+ Assert.IsFalse(eh.Properties.ContainsKey(EventMetadata.TenantIdAttributeName));
Assert.AreEqual(p.Id, eh.Properties[EventMetadata.KeyPropertyName]);
- ed = eh.ToBeefEventData();
+ ed = (EventData) await new EventHubs.AzureEventHubsEventConverter { UseMessagingPropertiesForMetadata = true }.ConvertFromAsync(typeof(Person), eh);
Assert.IsNotNull(eh);
Assert.AreEqual("Subject", ed.Subject);
Assert.AreEqual("Action", ed.Action);
diff --git a/tests/Beef.Events.UnitTest/Subscribe/EventHubs/AzureStorageRepositoryTest.cs b/tests/Beef.Events.UnitTest/Subscribe/EventHubs/AzureStorageRepositoryTest.cs
index 8bc17a5de..effeed3f1 100644
--- a/tests/Beef.Events.UnitTest/Subscribe/EventHubs/AzureStorageRepositoryTest.cs
+++ b/tests/Beef.Events.UnitTest/Subscribe/EventHubs/AzureStorageRepositoryTest.cs
@@ -30,13 +30,15 @@ private IConfiguration GetConfig()
return _config;
}
- private static EventHubData CreateEventData(string offset, long seqNo)
+ private static async Task CreateEventDataAsync(string offset, long seqNo)
{
- var e = new EventData
+ var ed = new EventData
{
Subject = "Unit.Test",
Action = "Verify",
- }.ToEventHubsEventData();
+ };
+
+ var e = await new MicrosoftEventHubsEventConverter().ConvertToAsync(ed);
var type = typeof(AzureEventHubs.EventData);
var pi = type.GetProperty("SystemProperties");
@@ -48,7 +50,9 @@ private static EventHubData CreateEventData(string offset, long seqNo)
dict.Add("x-opt-sequence-number", seqNo);
dict.Add("x-opt-partition-key", "0");
- return new EventHubData("testhub", "$Default", "0", e);
+ var ehd = new EventHubData("testhub", "$Default", "0", e);
+ ((IEventSubscriberData)ehd).SetMetadata(ed);
+ return ehd;
}
public static async Task> GetAuditRecords(CloudTable ct)
@@ -94,7 +98,7 @@ public async Task A100_EndToEnd()
var amt = await asr.GetAuditMessageTableAsync().ConfigureAwait(false);
await DeleteAuditRecords(amt).ConfigureAwait(false);
- var ed = CreateEventData("100", 1);
+ var ed = await CreateEventDataAsync("100", 1);
// Checking and removing with unknown is a-ok.
var ar = await asr.CheckPoisonedAsync(ed).ConfigureAwait(false);
@@ -214,7 +218,7 @@ public async Task A110_PoisonMaxAttempts()
var amt = await asr.GetAuditMessageTableAsync().ConfigureAwait(false);
await DeleteAuditRecords(amt).ConfigureAwait(false);
- var ed = CreateEventData("100", 1);
+ var ed = await CreateEventDataAsync("100", 1);
// Add events as poison.
await asr.MarkAsPoisonedAsync(ed, HandleResult(Result.DataNotFound("Data not found.")), 3).ConfigureAwait(false);
@@ -269,7 +273,7 @@ public async Task A120_PoisonMismatch()
var amt = await asr.GetAuditMessageTableAsync().ConfigureAwait(false);
await DeleteAuditRecords(amt).ConfigureAwait(false);
- var ed = CreateEventData("100", 1);
+ var ed = await CreateEventDataAsync("100", 1);
// Add an event as poison.
await asr.MarkAsPoisonedAsync(ed, HandleResult(Result.DataNotFound("Data not found."))).ConfigureAwait(false);
@@ -295,7 +299,7 @@ public async Task A120_PoisonMismatch()
Assert.AreEqual(1, ear.Attempts);
// Pretend to check a different event to that poisoned.
- ed = CreateEventData("200", 2);
+ ed = await CreateEventDataAsync("200", 2);
ar = await asr.CheckPoisonedAsync(ed).ConfigureAwait(false);
Assert.AreEqual(PoisonMessageAction.NotPoison, ar.Action);
Assert.AreEqual(0, ar.Attempts);
diff --git a/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs b/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs
index 71f1b5d23..d36f98aeb 100644
--- a/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs
+++ b/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs
@@ -9,6 +9,7 @@ namespace Beef.Template.Solution.UnitTest
[TestFixture]
public class TemplateTest
{
+ private static bool _firstTime = true;
private static DirectoryInfo _rootDir;
private static DirectoryInfo _unitTests;
@@ -45,7 +46,7 @@ private static (int exitCode, string stdOut) ExecuteCommand(string filename, str
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
- TestContext.WriteLine(line);
+ TestContext.Error.WriteLine(line);
}
process.WaitForExit();
@@ -55,9 +56,13 @@ private static (int exitCode, string stdOut) ExecuteCommand(string filename, str
return (process.ExitCode, sb.ToString());
}
- [OneTimeSetUp]
public void OneTimeSetUp()
{
+ if (!_firstTime)
+ return;
+
+ _firstTime = false;
+
// Determine directories.
_rootDir = new DirectoryInfo(TestContext.CurrentContext.WorkDirectory);
while (_rootDir.Name != "Beef")
@@ -85,28 +90,31 @@ public void OneTimeSetUp()
}
// Build Beef and package (nuget) - only local package, no deployment.
- Assert.GreaterOrEqual(0, ExecuteCommand("powershell", $"{Path.Combine(_rootDir.FullName, "nuget-publish.ps1")} packageonly").exitCode);
+ Assert.GreaterOrEqual(0, ExecuteCommand("powershell", $"{Path.Combine(_rootDir.FullName, "nuget-publish.ps1")} packageonly").exitCode, "nuget publish");
// Install the Beef template solution from local package.
// dotnet new -i beef.template.solution --nuget-source https://api.nuget.org/v3/index.json
- Assert.GreaterOrEqual(0, ExecuteCommand("dotnet", $"new -i beef.template.solution --nuget-source {Path.Combine(_rootDir.FullName, "nuget-publish")}").exitCode);
+ Assert.GreaterOrEqual(0, ExecuteCommand("dotnet", $"new -i beef.template.solution --nuget-source {Path.Combine(_rootDir.FullName, "nuget-publish")}").exitCode, "install beef.template.solution");
}
[Test]
public void Database()
{
+ OneTimeSetUp();
SolutionCreateGenerateTest("Foo.Db", "Bar", "Database");
}
[Test]
public void EntityFramework()
{
+ OneTimeSetUp();
SolutionCreateGenerateTest("Foo.Ef", "Bar", "EntityFramework");
}
[Test]
public void CosmosDb()
{
+ OneTimeSetUp();
SolutionCreateGenerateTest("Foo.Co", "Bar", "Cosmos");
}
diff --git a/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj b/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj
index 23569b1d6..c96f0215e 100644
--- a/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj
+++ b/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj
@@ -4,7 +4,7 @@
Exenetcoreapp3.1Beef.CodeGen
- 4.1.22
+ 4.1.23false
@@ -43,10 +43,11 @@
-
+
+
@@ -122,7 +123,8 @@
-
+
+
diff --git a/tools/Beef.CodeGen.Core/CHANGELOG.md b/tools/Beef.CodeGen.Core/CHANGELOG.md
index 0805f2485..c585381c9 100644
--- a/tools/Beef.CodeGen.Core/CHANGELOG.md
+++ b/tools/Beef.CodeGen.Core/CHANGELOG.md
@@ -2,6 +2,16 @@
Represents the **NuGet** versions.
+## v4.1.23
+- *Enhancement:* Added support for _YAML_ files with a `.yml` suffix.
+- *Enhancement:* Added support for `EventData.Source` to both entity and database (CDC) code-gen.
+- *Enhancement:* Added `Operation.ManagerExtensions`, `Operation.DataSvcExtensions` and `Operation.DataExtensions` to further control extensions code-gen output versus all for the `Entity`.
+- *Enhancement:* Added `CdcBackgroundServiceExtensions` to CDC code-gen.
+- *Enhancement:* An entity instance with the key will be instantiated for a `Delete` event within the `XxxDataSvc` to include as the event value.
+- *Fixed:* Code-gen for `ServiceCollectionsValidationExtension` was generating errant code when there are no validators.
+- *Enhancement:* Will strip out `bin/debug` and `bin/release` folders from default directory path to find the output directory; meaning the path does not need to be explicity set when running/debugging from Visual Studio.
+- *Fixed:* Issue [124](https://github.com/Avanade/Beef/issues/124) fixed. Corrected XML to YAML code-gen to handle values that start with `[` and end with `]` characters; including where used to represent arrays.
+
## v4.1.22
- *Enhancement:* Added standardized identifier mapping for change-data-capture (CDC) from local (internal) to global (external) where required.
- *Enhancement:* Added additional statistics information to console output.
diff --git a/tools/Beef.CodeGen.Core/CodeGenConsoleWrapper.cs b/tools/Beef.CodeGen.Core/CodeGenConsoleWrapper.cs
index 8b61648f9..5a7d02e15 100644
--- a/tools/Beef.CodeGen.Core/CodeGenConsoleWrapper.cs
+++ b/tools/Beef.CodeGen.Core/CodeGenConsoleWrapper.cs
@@ -3,6 +3,7 @@
using McMaster.Extensions.CommandLineUtils;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Reflection;
using System.Threading.Tasks;
@@ -17,30 +18,31 @@ public class CodeGenConsoleWrapper
private string _refDataScript = "RefDataCoreCrud.xml";
private string _dataModelScript = "DataModelOnly.xml";
private string _databaseScript = "Database.xml";
+ private string _exeDir;
///
/// Gets or sets the command line template.
///
public static string EntityCommandLineTemplate { get; set; }
- = "-s {{Script}} -o {{OutDir}} -p Company={{Company}} -p AppName={{AppName}} -p ApiName={{ApiName}}";
+ = "-s {{Script}} -o \"{{OutDir}}\" -p Company={{Company}} -p AppName={{AppName}} -p ApiName={{ApiName}}";
///
/// Gets or sets the command line template.
///
public static string DatabaseCommandLineTemplate { get; set; }
- = "-s {{Script}} -o {{OutDir}} -p Company={{Company}} -p AppName={{AppName}} -p AppDir={{AppName}}";
+ = "-s {{Script}} -o \"{{OutDir}}\" -p Company={{Company}} -p AppName={{AppName}} -p AppDir={{AppName}}";
///
/// Gets or sets the command line template.
///
public static string RefDataCommandLineTemplate { get; set; }
- = "-s {{Script}} -o {{OutDir}} -p Company={{Company}} -p AppName={{AppName}} -p ApiName={{ApiName}}";
+ = "-s {{Script}} -o \"{{OutDir}}\" -p Company={{Company}} -p AppName={{AppName}} -p ApiName={{ApiName}}";
///
/// Gets or sets the command line template.
///
public static string DataModelCommandLineTemplate { get; set; }
- = "-s {{Script}} -o {{OutDir}} -p Company={{Company}} -p AppName={{AppName}}";
+ = "-s {{Script}} -o \"{{OutDir}}\" -p Company={{Company}} -p AppName={{AppName}}";
///
/// Gets or sets the portion command line template.
@@ -55,7 +57,7 @@ public class CodeGenConsoleWrapper
/// The Web API name.
/// The output path/directory.
/// The instance.
- public static CodeGenConsoleWrapper Create(string company, string appName, string apiName = "Api", string outDir = "./..")
+ public static CodeGenConsoleWrapper Create(string company, string appName, string apiName = "Api", string? outDir = null)
{
return new CodeGenConsoleWrapper(new Assembly[] { Assembly.GetCallingAssembly() }, company, appName, apiName, outDir);
}
@@ -69,7 +71,7 @@ public static CodeGenConsoleWrapper Create(string company, string appName, strin
/// The Web API name.
/// The output path/directory.
/// The instance.
- public static CodeGenConsoleWrapper Create(Assembly[] assemblies, string company, string appName, string apiName = "Api", string outDir = "./..")
+ public static CodeGenConsoleWrapper Create(Assembly[] assemblies, string company, string appName, string apiName = "Api", string? outDir = null)
{
return new CodeGenConsoleWrapper(assemblies, company, appName, apiName, outDir);
}
@@ -82,12 +84,14 @@ public static CodeGenConsoleWrapper Create(Assembly[] assemblies, string company
/// The Web API name.
/// The output path/directory.
/// Optional list of assemblies to probe for resources.
- private CodeGenConsoleWrapper(Assembly[] assemblies, string company, string appName, string apiName = "Api", string outDir = "./..")
+ private CodeGenConsoleWrapper(Assembly[] assemblies, string company, string appName, string apiName = "Api", string? outDir = null)
{
Company = Check.NotEmpty(company, nameof(company));
AppName = Check.NotEmpty(appName, nameof(appName));
ApiName = Check.NotEmpty(apiName, nameof(apiName));
- OutDir = Check.NotEmpty(outDir, nameof(outDir));
+
+ _exeDir = CodeGenFileManager.GetExeDirectory();
+ OutDir = string.IsNullOrEmpty(outDir) ? new DirectoryInfo(_exeDir).Parent.FullName : outDir;
Assemblies = new List(assemblies ?? Array.Empty());
}
@@ -244,23 +248,23 @@ public async Task RunAsync(string[] args)
throw new CommandParsingException(app, "Command 'All' is not compatible with --xmlToYaml; the command must be more specific when converting XML configuration to YAML.");
CodeGenConsole.WriteMasthead();
- return await CodeGenFileManager.ConvertXmlToYamlAsync(ct, cfn ?? CodeGenFileManager.GetConfigFilename(ct, Company, AppName)).ConfigureAwait(false);
+ return await CodeGenFileManager.ConvertXmlToYamlAsync(ct, cfn ?? CodeGenFileManager.GetConfigFilename(_exeDir, ct, Company, AppName)).ConfigureAwait(false);
}
var encArg = enc.HasValue() ? " --expectNoChanges" : string.Empty;
var rc = 0;
if (IsDatabaseSupported && ct.HasFlag(CommandType.Database))
- rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache(cfn ?? CodeGenFileManager.GetConfigFilename(CommandType.Database, Company, AppName) + " " + DatabaseCommandLineTemplate, sfn ?? _databaseScript) + (cs.HasValue() ? $" -p \"ConnectionString={cs.Value()}\"" : "") + encArg)).ConfigureAwait(false);
+ rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache($"\"{cfn ?? CodeGenFileManager.GetConfigFilename(_exeDir, CommandType.Database, Company, AppName)}\"" + " " + DatabaseCommandLineTemplate, sfn ?? _databaseScript) + (cs.HasValue() ? $" -p \"ConnectionString={cs.Value()}\"" : "") + encArg)).ConfigureAwait(false);
if (rc == 0 && IsRefDataSupported && ct.HasFlag(CommandType.RefData))
- rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache(cfn ?? CodeGenFileManager.GetConfigFilename(CommandType.RefData, Company, AppName) + " " + RefDataCommandLineTemplate, sfn ?? _refDataScript) + encArg)).ConfigureAwait(false);
+ rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache($"\"{cfn ?? CodeGenFileManager.GetConfigFilename(_exeDir, CommandType.RefData, Company, AppName)}\"" + " " + RefDataCommandLineTemplate, sfn ?? _refDataScript) + encArg)).ConfigureAwait(false);
if (rc == 0 && IsEntitySupported && ct.HasFlag(CommandType.Entity))
- rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache(cfn ?? CodeGenFileManager.GetConfigFilename(CommandType.Entity, Company, AppName) + " " + EntityCommandLineTemplate, sfn ?? _entityScript) + encArg)).ConfigureAwait(false);
+ rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache($"\"{cfn ?? CodeGenFileManager.GetConfigFilename(_exeDir, CommandType.Entity, Company, AppName)}\"" + " " + EntityCommandLineTemplate, sfn ?? _entityScript) + encArg)).ConfigureAwait(false);
if (rc == 0 && IsDataModelSupported && ct.HasFlag(CommandType.DataModel))
- rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache(cfn ?? CodeGenFileManager.GetConfigFilename(CommandType.DataModel, Company, AppName) + " " + DataModelCommandLineTemplate, sfn ?? _dataModelScript) + encArg)).ConfigureAwait(false);
+ rc = await CodeGenConsole.Create().RunAsync(AppendAssemblies(ReplaceMoustache($"\"{cfn ?? CodeGenFileManager.GetConfigFilename(_exeDir, CommandType.DataModel, Company, AppName)}\"" + " " + DataModelCommandLineTemplate, sfn ?? _dataModelScript) + encArg)).ConfigureAwait(false);
return rc;
});
diff --git a/tools/Beef.CodeGen.Core/CodeGenFileManager.cs b/tools/Beef.CodeGen.Core/CodeGenFileManager.cs
index b0ad6c8c3..6b5040728 100644
--- a/tools/Beef.CodeGen.Core/CodeGenFileManager.cs
+++ b/tools/Beef.CodeGen.Core/CodeGenFileManager.cs
@@ -21,38 +21,57 @@ public static class CodeGenFileManager
///
/// Gets the list of supported filenames (will search in order specified).
///
- public static List EntityFilenames { get; } = new List(new string[] { "entity.beef.yaml", "entity.beef.json", "entity.beef.xml", "{{Company}}.{{AppName}}.xml" });
+ public static List EntityFilenames { get; } = new List(new string[] { "entity.beef.yaml", "entity.beef.yml", "entity.beef.json", "entity.beef.xml", "{{Company}}.{{AppName}}.xml" });
///
/// Gets the list of supported filenames (will search in order specified).
///
- public static List RefDataFilenames { get; } = new List(new string[] { "refdata.beef.yaml", "refdata.beef.json", "refdata.beef.xml", "{{Company}}.RefData.xml" });
+ public static List RefDataFilenames { get; } = new List(new string[] { "refdata.beef.yaml", "refdata.beef.yml", "refdata.beef.json", "refdata.beef.xml", "{{Company}}.RefData.xml" });
///
/// Gets the list of supported filenames (will search in order specified).
///
- public static List DataModelFilenames { get; } = new List(new string[] { "datamodel.beef.yaml", "datamodel.beef.json", "datamodel.beef.xml", "{{Company}}.{{AppName}}.DataModel.xml" });
+ public static List DataModelFilenames { get; } = new List(new string[] { "datamodel.beef.yaml", "datamodel.beef.yml", "datamodel.beef.json", "datamodel.beef.xml", "{{Company}}.{{AppName}}.DataModel.xml" });
///
/// Gets the list of supported filenames (will search in order specified).
///
- public static List DatabaseFilenames { get; } = new List(new string[] { "database.beef.yaml", "database.beef.json", "database.beef.xml", "{{Company}}.{{AppName}}.Database.xml" });
+ public static List DatabaseFilenames { get; } = new List(new string[] { "database.beef.yaml", "database.beef.yml", "database.beef.json", "database.beef.xml", "{{Company}}.{{AppName}}.Database.xml" });
+
+ ///
+ /// Gets the executable directory. Uses and removes bin/debug and bin/release where found to get back to root directory where configuration etc. should reside.
+ ///
+ /// The executable directory path.
+ public static string GetExeDirectory()
+ {
+ var exeDir = Environment.CurrentDirectory;
+ var i = exeDir.IndexOf(Path.Combine("bin", "debug"), StringComparison.InvariantCultureIgnoreCase);
+ if (i > 0)
+ exeDir = exeDir[0..i];
+
+ i = exeDir.IndexOf(Path.Combine("bin", "release"), StringComparison.InvariantCultureIgnoreCase);
+ if (i > 0)
+ exeDir = exeDir[0..i];
+
+ return exeDir;
+ }
///
/// Get the configuration filename.
///
+ /// The directory/path.
/// The .
/// The company name.
/// The application name.
/// The filename
- public static string GetConfigFilename(CommandType type, string company, string appName)
+ public static string GetConfigFilename(string directory, CommandType type, string company, string appName)
{
List files = new List();
foreach (var n in GetConfigFilenames(type))
{
- var fi = new FileInfo(n.Replace("{{Company}}", company, StringComparison.OrdinalIgnoreCase).Replace("{{AppName}}", appName, StringComparison.OrdinalIgnoreCase));
+ var fi = new FileInfo(Path.Combine(directory, n.Replace("{{Company}}", company, StringComparison.OrdinalIgnoreCase).Replace("{{AppName}}", appName, StringComparison.OrdinalIgnoreCase)));
if (fi.Exists)
- return fi.Name;
+ return fi.FullName;
files.Add(fi.Name);
}
@@ -136,9 +155,7 @@ public static async Task ConvertXmlToYamlAsync(CommandType type, string fil
logger.LogInformation(string.Empty);
}
-#pragma warning disable CA1031 // Do not catch general exception types; is OK.
catch (Exception ex)
-#pragma warning restore CA1031
{
logger.LogError(ex.Message);
logger.LogInformation(string.Empty);
diff --git a/tools/Beef.CodeGen.Core/Config/Database/CdcConfig.cs b/tools/Beef.CodeGen.Core/Config/Database/CdcConfig.cs
index 2a68c49e6..3c3429902 100644
--- a/tools/Beef.CodeGen.Core/Config/Database/CdcConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Database/CdcConfig.cs
@@ -159,6 +159,22 @@ public class CdcConfig : ConfigBase, ITableReferen
Description = "Defaults to `IDatabase`.")]
public string? DatabaseName { get; set; }
+ ///
+ /// Gets or sets the URI event source.
+ ///
+ [JsonProperty("eventSource", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("CDC", Title = "The Event Source.",
+ Description = "Defaults to `ModelName` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified.")]
+ public string? EventSource { get; set; }
+
+ ///
+ /// Gets or sets the default formatting for the Source when an Event is published.
+ ///
+ [JsonProperty("eventSourceFormat", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The default formatting for the Source when an Event is published.", Options = new string[] { "NameOnly", "NameAndKey", "NameAndGlobalId" },
+ Description = "Defaults to `CodeGeneration.EventSourceFormat`.")]
+ public string? EventSourceFormat { get; set; }
+
///
/// Gets or sets the event subject.
///
@@ -184,17 +200,17 @@ public class CdcConfig : ConfigBase, ITableReferen
public List? IncludeColumnsOnDelete { get; set; }
///
- /// The option to exclude the generation of the Background Service class (XxxBackgroundService.cs).
+ /// The option to exclude the generation of the CdcHostedService (background) class (XxxHostedService.cs).
///
- [JsonProperty("excludeBackgroundService", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DotNet", Title = "The option to exclude the generation of the `BackgroundService` class (`XxxBackgroundService.cs`).", IsImportant = true, Options = new string[] { NoOption, YesOption })]
- public string? ExcludeBackgroundService { get; set; }
+ [JsonProperty("excludeHostedService", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DotNet", Title = "The option to exclude the generation of the `CdcHostedService` (background) class (`XxxHostedService.cs`).", IsImportant = true, Options = new string[] { NoOption, YesOption })]
+ public string? ExcludeHostedService { get; set; }
///
/// Gets or sets the list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).
///
[JsonProperty("excludeColumnsFromETag", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DotNet", Title = "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
+ [PropertyCollectionSchema("DotNet", Title = "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
Description = "Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.")]
public List? ExcludeColumnsFromETag { get; set; }
@@ -388,6 +404,11 @@ public class CdcConfig : ConfigBase, ITableReferen
///
public string? QualifiedName => DbTable!.QualifiedName;
+ ///
+ /// Gets the event source URI.
+ ///
+ public string EventSourceUri => Root!.EventSourceRoot + (EventSource!.StartsWith('/') || (Root!.EventSourceRoot != null && Root!.EventSourceRoot.EndsWith('/')) ? EventSource : ("/" + EventSource));
+
///
/// Indicates whether there is at least one global identifier being used somewhere.
///
@@ -421,11 +442,13 @@ protected override void Prepare()
CdcSchema = DefaultWhereNull(CdcSchema, () => Root.CdcSchema);
OutboxTableName = DefaultWhereNull(OutboxTableName, () => Name + "Outbox");
ModelName = DefaultWhereNull(ModelName, () => Root.RenameForDotNet(Name));
+ EventSource = DefaultWhereNull(EventSource, () => ModelName!.ToLowerInvariant());
+ EventSourceFormat = DefaultWhereNull(EventSourceFormat, () => Root!.EventSourceFormat);
EventSubject = DefaultWhereNull(EventSubject, () => ModelName);
EventSubjectFormat = DefaultWhereNull(EventSubjectFormat, () => Root!.EventSubjectFormat);
DataCtor = DefaultWhereNull(DataCtor, () => "Public");
DatabaseName = DefaultWhereNull(DatabaseName, () => "IDatabase");
- ExcludeBackgroundService = DefaultWhereNull(ExcludeBackgroundService, () => NoOption);
+ ExcludeHostedService = DefaultWhereNull(ExcludeHostedService, () => NoOption);
if (ExcludeColumnsFromETag == null && Root!.CdcExcludeColumnsFromETag != null)
ExcludeColumnsFromETag = new List(Root!.CdcExcludeColumnsFromETag!);
diff --git a/tools/Beef.CodeGen.Core/Config/Database/CdcJoinConfig.cs b/tools/Beef.CodeGen.Core/Config/Database/CdcJoinConfig.cs
index 0b9b45918..3d5cb846c 100644
--- a/tools/Beef.CodeGen.Core/Config/Database/CdcJoinConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Database/CdcJoinConfig.cs
@@ -171,7 +171,7 @@ public class CdcJoinConfig : ConfigBase, ITableReferen
/// Gets or sets the list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).
///
[JsonProperty("excludeColumnsFromETag", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DotNet", Title = "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
+ [PropertyCollectionSchema("DotNet", Title = "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
Description = "Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.")]
public List? ExcludeColumnsFromETag { get; set; }
diff --git a/tools/Beef.CodeGen.Core/Config/Database/CodeGenConfig.cs b/tools/Beef.CodeGen.Core/Config/Database/CodeGenConfig.cs
index 6e63672af..88f4fa4f8 100644
--- a/tools/Beef.CodeGen.Core/Config/Database/CodeGenConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Database/CodeGenConfig.cs
@@ -192,15 +192,39 @@ public class CodeGenConfig : ConfigBase, IRootConf
/// Gets or sets the default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).
///
[JsonProperty("cdcExcludeColumnsFromETag", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DotNet", Title = "The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)")]
+ [PropertyCollectionSchema("DotNet", Title = "The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)")]
public List? CdcExcludeColumnsFromETag { get; set; }
+ ///
+ /// Gets or sets the URI root for the event source by prepending to all event source URIs.
+ ///
+ [JsonProperty("eventSourceRoot", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("CDC", Title = "The URI root for the event source by prepending to all event source URIs.",
+ Description = "The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).")]
+ public string? EventSourceRoot { get; set; }
+
+ ///
+ /// Gets or sets the URI kind for the event source URIs.
+ ///
+ [JsonProperty("eventSourceKind", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("CDC", Title = "The URI kind for the event source URIs.", Options = new string[] { "None", "Absolute", "Relative", "RelativeOrAbsolute" },
+ Description = "Defaults to `None` (being the event source is not updated).")]
+ public string? EventSourceKind { get; set; }
+
+ ///
+ /// Gets or sets the default formatting for the Source when an Event is published.
+ ///
+ [JsonProperty("eventSourceFormat", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The default formatting for the Source when an Event is published.", Options = new string[] { "NameOnly", "NameAndKey", "NameAndGlobalId" },
+ Description = "Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.")]
+ public string? EventSourceFormat { get; set; }
+
///
/// Gets or sets the root for the event name by prepending to all event subject names.
///
[JsonProperty("eventSubjectRoot", DefaultValueHandling = DefaultValueHandling.Ignore)]
[PropertySchema("CDC", Title = "The root for the event name by prepending to all event subject names.",
- Description = "Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).", IsImportant = true)]
+ Description = "Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be extended within the `Entity`(s).", IsImportant = true)]
public string? EventSubjectRoot { get; set; }
///
@@ -215,8 +239,8 @@ public class CodeGenConfig : ConfigBase, IRootConf
/// Gets or sets the formatting for the Action when an Event is published.
///
[JsonProperty("eventActionFormat", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("CDC", Title = "The formatting for the Action when an Event is published.", Options = new string[] { "None", "UpperCase", "PastTense", "PastTenseUpperCase" }, IsImportant = true,
- Description = "Defaults to `None` (no formatting required).")]
+ [PropertySchema("CDC", Title = "The formatting for the Action when an Event is published.", Options = new string[] { "None", "PastTense" }, IsImportant = true,
+ Description = "Defaults to `None` (no formatting required, i.e. as-is).")]
public string? EventActionFormat { get; set; }
///
@@ -486,6 +510,8 @@ protected override void Prepare()
CdcIdentifierMappingTableName = DefaultWhereNull(CdcIdentifierMappingTableName, () => "CdcIdentifierMapping");
CdcIdentifierMappingStoredProcedureName = DefaultWhereNull(CdcIdentifierMappingStoredProcedureName, () => "spCreateCdcIdentifierMapping");
HasBeefDbo = DefaultWhereNull(HasBeefDbo, () => true);
+ EventSourceKind = DefaultWhereNull(EventSourceKind, () => "None");
+ EventSourceFormat = DefaultWhereNull(EventSourceFormat, () => "NameAndKey");
EventSubjectFormat = DefaultWhereNull(EventSubjectFormat, () => "NameAndKey");
EventActionFormat = DefaultWhereNull(EventActionFormat, () => "None");
JsonSerializer = DefaultWhereNull(JsonSerializer, () => "Newtonsoft");
diff --git a/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs
index 9aebdfefc..85b6d3329 100644
--- a/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs
@@ -274,6 +274,22 @@ public class CodeGenConfig : ConfigBase, IRootConf
Description = "Defaults to `true`. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).")]
public bool? EventPublish { get; set; }
+ ///
+ /// Gets or sets the URI root for the event source by prepending to all event source URIs.
+ ///
+ [JsonProperty("eventSourceRoot", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The URI root for the event source by prepending to all event source URIs.",
+ Description = "The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).")]
+ public string? EventSourceRoot { get; set; }
+
+ ///
+ /// Gets or sets the URI kind for the event source URIs.
+ ///
+ [JsonProperty("eventSourceKind", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The URI kind for the event source URIs.", Options = new string[] { "None", "Absolute", "Relative", "RelativeOrAbsolute" },
+ Description = "Defaults to `None` (being the event source is not updated).")]
+ public string? EventSourceKind { get; set; }
+
///
/// Gets or sets the root for the event Subject name by prepending to all event subject names.
///
@@ -290,12 +306,20 @@ public class CodeGenConfig : ConfigBase, IRootConf
Description = "Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.")]
public string? EventSubjectFormat { get; set; }
+ ///
+ /// Gets or sets the subject path separator.
+ ///
+ [JsonProperty("eventSubjectSeparator", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The subject path separator.",
+ Description = "Defaults to `.`. Used only where the subject is automatically inferred.")]
+ public string? EventSubjectSeparator { get; set; }
+
///
/// Gets or sets the formatting for the Action when an Event is published.
///
[JsonProperty("eventActionFormat", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DataSvc", Title = "The formatting for the Action when an Event is published.", Options = new string[] { "None", "UpperCase", "PastTense", "PastTenseUpperCase" }, IsImportant = true,
- Description = "Defaults to `None` (no formatting required)`.")]
+ [PropertySchema("DataSvc", Title = "The formatting for the Action when an Event is published.", Options = new string[] { "None", "PastTense" }, IsImportant = true,
+ Description = "Defaults to `None` (no formatting required, i.e. as-is)`.")]
public string? EventActionFormat { get; set; }
///
@@ -559,7 +583,9 @@ protected override void Prepare()
WebApiAutoLocation = DefaultWhereNull(WebApiAutoLocation, () => false);
RefDataCache = DefaultWhereNull(RefDataCache, () => "ReferenceDataCache");
ValidatorLayer = DefaultWhereNull(ValidatorLayer, () => "Business");
+ EventSourceKind = DefaultWhereNull(EventSourceKind, () => "None");
EventSubjectFormat = DefaultWhereNull(EventSubjectFormat, () => "NameAndKey");
+ EventSubjectSeparator = DefaultWhereNull(EventSubjectSeparator, () => ".");
EventPublish = DefaultWhereNull(EventPublish, () => true);
EventActionFormat = DefaultWhereNull(EventActionFormat, () => "None");
EntityUsing = DefaultWhereNull(EntityUsing, () => "Common");
diff --git a/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs
index 679228251..22c2a48f9 100644
--- a/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs
@@ -351,7 +351,8 @@ public class EntityConfig : ConfigBase
/// Indicates whether the `Data` extensions logic should be generated.
///
[JsonProperty("dataExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("Data", Title = "Indicates whether the `Data` extensions logic should be generated.")]
+ [PropertySchema("Data", Title = "Indicates whether the `Data` extensions logic should be generated.",
+ Description = "This can be overridden using `Operation.DataExtensions`.")]
public bool? DataExtensions { get; set; }
#endregion
@@ -542,6 +543,15 @@ public class EntityConfig : ConfigBase
Description = "Defaults to the `CodeGeneration.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.")]
public bool? EventPublish { get; set; }
+ ///
+ /// Gets or sets the URI event source.
+ ///
+ [JsonProperty("eventSource", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The Event Source.",
+ Description = "Defaults to `Name` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. " +
+ "To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.")]
+ public string? EventSource { get; set; }
+
///
/// Gets or sets the default formatting for the Subject when an Event is published.
///
@@ -581,7 +591,8 @@ public class EntityConfig : ConfigBase
/// Indicates whether the `DataSvc` extensions logic should be generated.
///
[JsonProperty("dataSvcExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("DataSvc", Title = "Indicates whether the `DataSvc` extensions logic should be generated.")]
+ [PropertySchema("DataSvc", Title = "Indicates whether the `DataSvc` extensions logic should be generated.",
+ Description = "This can be overridden using `Operation.DataSvcExtensions`.")]
public bool? DataSvcExtensions { get; set; }
#endregion
@@ -609,7 +620,8 @@ public class EntityConfig : ConfigBase
/// Indicates whether the `Manager` extensions logic should be generated.
///
[JsonProperty("managerExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
- [PropertySchema("Manager", Title = "Indicates whether the `Manager` extensions logic should be generated.")]
+ [PropertySchema("Manager", Title = "Indicates whether the `Manager` extensions logic should be generated.",
+ Description = "This can be overridden using `Operation.ManagerExtensions`.")]
public bool? ManagerExtensions { get; set; }
///
@@ -892,6 +904,21 @@ public class EntityConfig : ConfigBase
///
public List? DataSvcAutoOperations => Operations!.Where(x => IsNoOption(x.ExcludeDataSvc) && CompareNullOrValue(x.DataSvcCustom, false)).ToList();
+ ///
+ /// Indicates where there are any .
+ ///
+ public bool HasManagerExtensions => Operations.Any(x => x.ManagerExtensions == true);
+
+ ///
+ /// Indicates where there are any .
+ ///
+ public bool HasDataSvcExtensions => Operations.Any(x => x.DataSvcExtensions == true);
+
+ ///
+ /// Indicates where there are any .
+ ///
+ public bool HasDataExtensions => Operations.Any(x => x.DataExtensions == true);
+
///
/// Gets the Manager constructor parameters.
///
@@ -1042,7 +1069,7 @@ public class EntityConfig : ConfigBase
///
/// Indicates whether the data extensions section is required.
///
- public bool DataExtensionsRequired => CompareValue(DataExtensions, true) || UsesCosmos || DataOperations.Any(x => x.Type == "GetColl");
+ public bool DataExtensionsRequired => HasDataExtensions || UsesCosmos || DataOperations.Any(x => x.Type == "GetColl");
///
/// Gets the reference data qualified Entity name.
@@ -1082,6 +1109,7 @@ protected override void Prepare()
ODataName = InterfaceiseName(DefaultWhereNull(ODataName, () => Parent!.ODataName));
DataSvcCaching = DefaultWhereNull(DataSvcCaching, () => true);
DataSvcCtor = DefaultWhereNull(DataSvcCtor, () => "Public");
+ EventSource = DefaultWhereNull(EventSource, () => Name!.ToLowerInvariant());
EventPublish = DefaultWhereNull(EventPublish, () => Parent!.EventPublish);
EventTransaction = DefaultWhereNull(EventTransaction, () => Parent!.EventTransaction);
ManagerCtor = DefaultWhereNull(ManagerCtor, () => "Public");
diff --git a/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs
index 5fff09f38..4cc9c3b8b 100644
--- a/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs
+++ b/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs
@@ -1,10 +1,10 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef
-using Beef.Events;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
namespace Beef.CodeGen.Config.Entity
@@ -155,6 +155,14 @@ public class OperationConfig : ConfigBase
Description = "Used where the default generated `Mapper` is not applicable.")]
public string? DataEntityMapper { get; set; }
+ ///
+ /// Indicates whether the `Data` extensions logic should be generated.
+ ///
+ [JsonProperty("dataExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("Data", Title = "Indicates whether the `Data` extensions logic should be generated.",
+ Description = "Defaults to `Entity.DataExtensions`.")]
+ public bool? DataExtensions { get; set; }
+
///
/// Gets or sets the database stored procedure name.
///
@@ -205,6 +213,14 @@ public class OperationConfig : ConfigBase
[PropertySchema("Manager", Title = "Indicates whether a `System.TransactionScope` should be created and orchestrated at the `Manager`-layer.")]
public bool? ManagerTransaction { get; set; }
+ ///
+ /// Indicates whether the `Manager` extensions logic should be generated.
+ ///
+ [JsonProperty("managerExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("Data", Title = "Indicates whether the `Manager` extensions logic should be generated.",
+ Description = "Defaults to `Entity.ManagerExtensions`.")]
+ public bool? ManagerExtensions { get; set; }
+
///
/// Gets or sets the name of the .NET Type that will perform the validation.
///
@@ -247,6 +263,14 @@ public class OperationConfig : ConfigBase
[PropertySchema("DataSvc", Title = "Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer.")]
public bool? DataSvcTransaction { get; set; }
+ ///
+ /// Indicates whether the `DataSvc` extensions logic should be generated.
+ ///
+ [JsonProperty("dataSvcExtensions", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "Indicates whether the `DataSvc` extensions logic should be generated.",
+ Description = "Defaults to `Entity.ManagerExtensions`.")]
+ public bool? DataSvcExtensions { get; set; }
+
///
/// Indicates whether to add logic to publish an event on the successful completion of the DataSvc layer invocation for a Create, Update or Delete operation.
///
@@ -255,6 +279,15 @@ public class OperationConfig : ConfigBase
Description = "Defaults to the `CodeGeneration.EventPublish` or `Entity.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.")]
public bool? EventPublish { get; set; }
+ ///
+ /// Gets or sets the URI event source.
+ ///
+ [JsonProperty("eventSource", DefaultValueHandling = DefaultValueHandling.Ignore)]
+ [PropertySchema("DataSvc", Title = "The Event Source.",
+ Description = "Defaults to `Entity.EventSource`. Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. " +
+ "To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.")]
+ public string? EventSource { get; set; }
+
///
/// Gets or sets the event subject template and corresponding event action pair (separated by a colon).
///
@@ -502,10 +535,36 @@ public class OperationConfig : ConfigBase
///
public List? CleanerParameters => Parameters!.Where(x => !x.LayerPassing!.StartsWith("ToManager", StringComparison.OrdinalIgnoreCase) && !x.IsPagingArgs).ToList();
+ ///
+ /// The operation event properties.
+ ///
+ public class OperationEvent
+ {
+ ///
+ /// Gets or sets the event subject.
+ ///
+ public string? Subject { get; set; }
+
+ ///
+ /// Gets or sets the event action.
+ ///
+ public string? Action { get; set; }
+
+ ///
+ /// Gets or sets the event source.
+ ///
+ public string? Source { get; set; }
+
+ ///
+ /// Gets or sets the event value (if any).
+ ///
+ public string? Value { get; set; }
+ }
+
///
/// Gets the list of events derived from the .
///
- public List Events { get; } = new List();
+ public List Events { get; } = new List();
///
/// Gets the formatted summary text.
@@ -602,6 +661,16 @@ public class OperationConfig : ConfigBase
///
public string? GrpcReturnMapper { get; set; }
+ ///
+ /// Gets the event source URI.
+ ///
+ public string EventSourceUri => Root!.EventSourceRoot + (EventSource!.StartsWith('/') || (Root!.EventSourceRoot != null && Root!.EventSourceRoot.EndsWith('/')) ? EventSource : ("/" + EventSource));
+
+ ///
+ /// Gets the event format key code.
+ ///
+ public string? EventFormatKey { get; private set; }
+
///
///
///
@@ -691,6 +760,8 @@ protected override void Prepare()
if (Type == "Custom")
AutoImplement = "None";
+ ManagerExtensions = DefaultWhereNull(ManagerExtensions, () => Parent!.ManagerExtensions);
+ DataExtensions = DefaultWhereNull(DataExtensions, () => Parent!.DataExtensions);
DataEntityMapperCreate = string.IsNullOrEmpty(DataEntityMapper);
DataEntityMapper = DefaultWhereNull(DataEntityMapper, () => AutoImplement switch
{
@@ -738,29 +809,27 @@ protected override void Prepare()
_ => "Unspecified"
});
+ EventSource = DefaultWhereNull(EventSource, () => Parent!.EventSource);
EventPublish = DefaultWhereNull(EventPublish, () => CompareValue(Parent!.EventPublish, true) && new string[] { "Create", "Update", "Delete" }.Contains(Type));
- EventSubject = DefaultWhereNull(EventSubject, () =>
+
+ EventFormatKey = Type switch
{
- var key = Parent!.EventSubjectFormat == "NameOnly" ? null : Type switch
- {
- "Create" => string.Join(",", Parent!.Properties.Where(p => p.UniqueKey.HasValue && p.UniqueKey.Value).Select(x => $"{{__result.{x.PropertyName}}}")),
- "Update" => string.Join(",", Parent!.Properties.Where(p => p.UniqueKey.HasValue && p.UniqueKey.Value).Select(x => $"{{__result.{x.PropertyName}}}")),
- "Delete" => string.Join(",", Parent!.Properties.Where(p => p.UniqueKey.HasValue && p.UniqueKey.Value).Select(x => $"{{{x.ArgumentName}}}")),
- _ => null
- };
+ "Create" => "{_evtPub.FormatKey(__result)}",
+ "Update" => "{_evtPub.FormatKey(__result)}",
+ "Delete" => $"{{_evtPub.FormatKey({string.Join(", ", Parent!.Properties.Where(p => p.UniqueKey.HasValue && p.UniqueKey.Value).Select(x => $"{x.ArgumentName}"))})}}",
+ _ => null
+ };
- return Type switch
- {
- "Create" => $"{Root!.AppName}.{Parent!.Name}{(key == null ? "" : "." + key)}:{ConvertEventAction(ManagerOperationType!)}",
- "Update" => $"{Root!.AppName}.{Parent!.Name}{(key == null ? "" : "." + key)}:{ConvertEventAction(ManagerOperationType!)}",
- "Delete" => $"{Root!.AppName}.{Parent!.Name}{(key == null ? "" : "." + key)}:{ConvertEventAction(ManagerOperationType!)}",
- _ => null
- };
+ EventSubject = DefaultWhereNull(EventSubject, () => Type switch
+ {
+ "Create" => $"{Root!.AppName}{Root!.EventSubjectSeparator}{Parent!.Name}{(EventFormatKey == null ? "" : $"{Root!.EventSubjectSeparator}" + EventFormatKey)}:{ConvertEventAction(ManagerOperationType!)}",
+ "Update" => $"{Root!.AppName}{Root!.EventSubjectSeparator}{Parent!.Name}{(EventFormatKey == null ? "" : $"{Root!.EventSubjectSeparator}" + EventFormatKey)}:{ConvertEventAction(ManagerOperationType!)}",
+ "Delete" => $"{Root!.AppName}{Root!.EventSubjectSeparator}{Parent!.Name}{(EventFormatKey == null ? "" : $"{Root!.EventSubjectSeparator}" + EventFormatKey)}:{ConvertEventAction(ManagerOperationType!)}",
+ _ => null
});
- PrepareEvents();
-
DataSvcTransaction = DefaultWhereNull(DataSvcTransaction, () => CompareValue(EventPublish, true) && CompareValue(Parent!.EventTransaction, true));
+ DataSvcExtensions = DefaultWhereNull(DataSvcExtensions, () => Parent!.DataSvcExtensions);
ExcludeIData = DefaultWhereNull(ExcludeIData, () => CompareValue(ExcludeAll, YesOption) ? YesOption : NoOption);
ExcludeData = DefaultWhereNull(ExcludeData, () => CompareValue(ExcludeAll, YesOption) ? YesOption : NoOption);
ExcludeIDataSvc = DefaultWhereNull(ExcludeIDataSvc, () => CompareValue(ExcludeAll, YesOption) ? YesOption : NoOption);
@@ -775,6 +844,7 @@ protected override void Prepare()
ExcludeIData = ExcludeData = ExcludeIDataSvc = ExcludeDataSvc = ExcludeIManager = ExcludeManager = YesOption;
PrepareParameters();
+ PrepareEvents();
WebApiRoute = DefaultWhereNull(WebApiRoute, () => Type switch
{
@@ -823,9 +893,7 @@ protected override void Prepare()
///
private string ConvertEventAction(string action) => Root!.EventActionFormat switch
{
- "UpperCase" => action.ToUpperInvariant(),
"PastTense" => StringConversion.ToPastTense(action)!,
- "PastTenseUpperCase" => StringConversion.ToPastTense(action)!.ToUpperInvariant(),
_ => action
};
@@ -869,7 +937,7 @@ private void PrepareEvents()
foreach (var @event in EventSubject!.Split(";", StringSplitOptions.RemoveEmptyEntries))
{
- var ed = new EventData();
+ var ed = new OperationEvent();
var parts = @event.Split(":");
if (parts.Length > 0)
ed.Subject = parts[0];
@@ -882,6 +950,28 @@ private void PrepareEvents()
if (Root!.EventSubjectRoot != null)
ed.Subject = Root!.EventSubjectRoot + "." + ed.Subject;
+ if (Root!.EventSourceKind != "None")
+ ed.Source = EventSourceUri.Replace("{$key}", EventFormatKey);
+
+ if (HasReturnValue)
+ ed.Value = "__result";
+ else if (Type == "Delete" && UniqueKey == true)
+ {
+ var sb = new StringBuilder();
+ foreach (var dp in DataParameters)
+ {
+ if (sb.Length == 0)
+ sb.Append($"new {Parent!.Name} {{ ");
+ else
+ sb.Append(", ");
+
+ sb.Append($"{dp.Name} = {dp.ArgumentName}");
+ }
+
+ sb.Append(" }");
+ ed.Value = sb.ToString();
+ }
+
Events.Add(ed);
}
}
diff --git a/tools/Beef.CodeGen.Core/Config/XmlYamlTranslate.cs b/tools/Beef.CodeGen.Core/Config/XmlYamlTranslate.cs
index 41f44feae..c32cbf6b2 100644
--- a/tools/Beef.CodeGen.Core/Config/XmlYamlTranslate.cs
+++ b/tools/Beef.CodeGen.Core/Config/XmlYamlTranslate.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
namespace Beef.CodeGen.Config
{
@@ -61,84 +62,84 @@ internal static class XmlYamlTranslate
(ConfigType.Database, ConfigurationEntity.Parameter, "IsCollection", "collection")
});
- private static readonly List<(ConfigType ConvertType, ConfigurationEntity Entity, string XmlName, Func Converter)> _xmlToYamlConvert = new List<(ConfigType, ConfigurationEntity, string, Func)>(new (ConfigType, ConfigurationEntity, string, Func)[]
+ private static readonly List<(ConfigType ConvertType, ConfigurationEntity Entity, string XmlName, bool IsArray, Func? Converter)> _xmlToYamlConvert = new List<(ConfigType, ConfigurationEntity, string, bool, Func?)>(new (ConfigType, ConfigurationEntity, string, bool, Func?)[]
{
- (ConfigType.Entity, ConfigurationEntity.CodeGen, "xmlns", (xml) => NullValue()),
- (ConfigType.Entity, ConfigurationEntity.CodeGen, "xsi", (xml) => NullValue()),
- (ConfigType.Entity, ConfigurationEntity.CodeGen, "noNamespaceSchemaLocation", (xml) => NullValue()),
- (ConfigType.Entity, ConfigurationEntity.CodeGen, "WebApiAuthorize", (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
-
- (ConfigType.Entity, ConfigurationEntity.Entity, "ManagerCtorParams", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Entity, ConfigurationEntity.Entity, "DataSvcCtorParams", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Entity, ConfigurationEntity.Entity, "DataCtorParams", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Entity, ConfigurationEntity.Entity, "WebApiCtorParams", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeEntity", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeAll", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIData", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeData", (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? ConfigBase.YesOption : "RequiresMapper")),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIDataSvc", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeDataSvc", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIManager", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeManager", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeWebApi", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeWebApiAgent", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeGrpcAgent", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Entity, "WebApiAuthorize", (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
-
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIData", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeData", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIDataSvc", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeDataSvc", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIManager", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeManager", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeWebApi", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeWebApiAgent", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeGrpcAgent", (xml) => ConvertBoolToYesNo(xml)),
- (ConfigType.Entity, ConfigurationEntity.Operation, "WebApiAuthorize", (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
- (ConfigType.Entity, ConfigurationEntity.Operation, "WebApiOperationType", (xml) => throw new CodeGenException("Operation.WebApiOperationType has been renamed; please change to Operation.ManagerOperationType.")),
-
- (ConfigType.Database, ConfigurationEntity.CodeGen, "xmlns", (xml) => NullValue()),
- (ConfigType.Database, ConfigurationEntity.CodeGen, "xsi", (xml) => NullValue()),
- (ConfigType.Database, ConfigurationEntity.CodeGen, "noNamespaceSchemaLocation", (xml) => NullValue()),
- (ConfigType.Database, ConfigurationEntity.CodeGen, "CdcExcludeColumnsFromETag", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
-
- (ConfigType.Database, ConfigurationEntity.Query, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Query, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Query, "AliasColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
-
- (ConfigType.Database, ConfigurationEntity.QueryJoin, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.QueryJoin, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.QueryJoin, "AliasColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
-
- (ConfigType.Database, ConfigurationEntity.Table, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Table, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Table, "GetAllOrderBy", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Table, "UdtExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Table, "View", (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
- (ConfigType.Database, ConfigurationEntity.Table, "ViewName", (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
- (ConfigType.Database, ConfigurationEntity.Table, "ViewSchema", (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
-
- (ConfigType.Database, ConfigurationEntity.StoredProcedure, "Type", (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "GetAll" ? "GetColl" : xml)),
- (ConfigType.Database, ConfigurationEntity.StoredProcedure, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.StoredProcedure, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.StoredProcedure, "MergeOverrideIdentityColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
-
- (ConfigType.Database, ConfigurationEntity.OrderBy, "Order", (xml) => string.IsNullOrEmpty(xml) ? null : (xml.StartsWith("Des", StringComparison.OrdinalIgnoreCase) ? "Descending" : "Ascending")),
-
- (ConfigType.Database, ConfigurationEntity.Cdc, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "AliasColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "DataCtorParams", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "IdentifierMappingColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "IncludeColumnsOnDelete", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.Cdc, "ExcludeColumnsFromETag", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
-
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "IncludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "ExcludeColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "AliasColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "IdentifierMappingColumns", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "IncludeColumnsOnDelete", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]"),
- (ConfigType.Database, ConfigurationEntity.CdcJoin, "ExcludeColumnsFromETag", (xml) => string.IsNullOrEmpty(xml) ? null : $"[ {xml} ]")
+ (ConfigType.Entity, ConfigurationEntity.CodeGen, "xmlns", false, (xml) => NullValue()),
+ (ConfigType.Entity, ConfigurationEntity.CodeGen, "xsi", false, (xml) => NullValue()),
+ (ConfigType.Entity, ConfigurationEntity.CodeGen, "noNamespaceSchemaLocation", false, (xml) => NullValue()),
+ (ConfigType.Entity, ConfigurationEntity.CodeGen, "WebApiAuthorize", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
+
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ManagerCtorParams", true, null),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "DataSvcCtorParams", true, null),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "DataCtorParams", true, null),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "WebApiCtorParams", true, null),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeEntity", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeAll", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIData", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeData", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? ConfigBase.YesOption : "RequiresMapper")),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIDataSvc", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeDataSvc", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeIManager", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeManager", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeWebApi", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeWebApiAgent", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "ExcludeGrpcAgent", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Entity, "WebApiAuthorize", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
+
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIData", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeData", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIDataSvc", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeDataSvc", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeIManager", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeManager", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeWebApi", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeWebApiAgent", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "ExcludeGrpcAgent", false, (xml) => ConvertBoolToYesNo(xml)),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "WebApiAuthorize", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? "Authorize" : (xml == "false" ? "AllowAnonymous" : xml))),
+ (ConfigType.Entity, ConfigurationEntity.Operation, "WebApiOperationType", false, (xml) => throw new CodeGenException("Operation.WebApiOperationType has been renamed; please change to Operation.ManagerOperationType.")),
+
+ (ConfigType.Database, ConfigurationEntity.CodeGen, "xmlns", false, (xml) => NullValue()),
+ (ConfigType.Database, ConfigurationEntity.CodeGen, "xsi", false, (xml) => NullValue()),
+ (ConfigType.Database, ConfigurationEntity.CodeGen, "noNamespaceSchemaLocation", false, (xml) => NullValue()),
+ (ConfigType.Database, ConfigurationEntity.CodeGen, "CdcExcludeColumnsFromETag", true, null),
+
+ (ConfigType.Database, ConfigurationEntity.Query, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Query, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Query, "AliasColumns", true, null),
+
+ (ConfigType.Database, ConfigurationEntity.QueryJoin, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.QueryJoin, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.QueryJoin, "AliasColumns", true, null),
+
+ (ConfigType.Database, ConfigurationEntity.Table, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Table, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Table, "GetAllOrderBy", true, null),
+ (ConfigType.Database, ConfigurationEntity.Table, "UdtExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Table, "View", false, (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
+ (ConfigType.Database, ConfigurationEntity.Table, "ViewName", false, (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
+ (ConfigType.Database, ConfigurationEntity.Table, "ViewSchema", false, (xml) => throw new CodeGenException("Table.View property is no longer supported; please use the new Query capability (more advanced).")),
+
+ (ConfigType.Database, ConfigurationEntity.StoredProcedure, "Type", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml == "GetAll" ? "GetColl" : xml)),
+ (ConfigType.Database, ConfigurationEntity.StoredProcedure, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.StoredProcedure, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.StoredProcedure, "MergeOverrideIdentityColumns", true, null),
+
+ (ConfigType.Database, ConfigurationEntity.OrderBy, "Order", false, (xml) => string.IsNullOrEmpty(xml) ? null : (xml.StartsWith("Des", StringComparison.OrdinalIgnoreCase) ? "Descending" : "Ascending")),
+
+ (ConfigType.Database, ConfigurationEntity.Cdc, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "AliasColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "DataCtorParams", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "IdentifierMappingColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "IncludeColumnsOnDelete", true, null),
+ (ConfigType.Database, ConfigurationEntity.Cdc, "ExcludeColumnsFromETag", true, null),
+
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "IncludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "ExcludeColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "AliasColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "IdentifierMappingColumns", true, null),
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "IncludeColumnsOnDelete", true, null),
+ (ConfigType.Database, ConfigurationEntity.CdcJoin, "ExcludeColumnsFromETag", true, null)
});
private static string? ConvertBoolToYesNo(string? xml) => string.IsNullOrEmpty(xml) ? null : (xml == "true" ? ConfigBase.YesOption : null);
@@ -319,7 +320,48 @@ internal static string GetXmlName(ConfigType convertType, ConfigurationEntity en
internal static string? GetYamlValue(ConfigType convertType, ConfigurationEntity entity, string xmlName, string? xmlValue)
{
var item = _xmlToYamlConvert.FirstOrDefault(x => x.ConvertType == convertType && x.Entity == entity && x.XmlName == xmlName);
- return item.Converter == null ? xmlValue : item.Converter(xmlValue);
+ var yaml = item.Converter == null ? xmlValue : item.Converter(xmlValue);
+ return item.IsArray ? FormatYamlArray(yaml) : FormatYamlValue(yaml);
+ }
+
+ ///
+ /// Check YAML for special characters and format accordingly.
+ ///
+ internal static string? FormatYamlValue(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return null;
+
+ if (value.IndexOfAny(new char[] { ':', '{', '}', '[', ']', ',', '&', '*', '#', '?', '|', '-', '<', '>', '=', '!', '%', '@', '\\', '\"', '\'' }) >= 0)
+ value = $"'{value.Replace("'", "''", StringComparison.InvariantCultureIgnoreCase)}'";
+
+ if (string.Compare(value, "NULL", StringComparison.InvariantCultureIgnoreCase) == 0)
+ value = $"'{value}'";
+
+ return value;
+ }
+
+ ///
+ /// Splits the string on a comma, and then formats each part and then bookends with square brackets.
+ ///
+ ///
+ ///
+ internal static string? FormatYamlArray(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return null;
+
+ var sb = new StringBuilder();
+ foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ {
+ sb.Append(sb.Length == 0 ? "[ " : ", ");
+ var yaml = FormatYamlValue(part);
+ if (!string.IsNullOrEmpty(yaml))
+ sb.Append(FormatYamlValue(part));
+ }
+
+ sb.Append(" ]");
+ return sb.ToString();
}
///
diff --git a/tools/Beef.CodeGen.Core/Converters/XmlToYamlConverter.cs b/tools/Beef.CodeGen.Core/Converters/XmlToYamlConverter.cs
index ab0c1bb5a..4b161fa1b 100644
--- a/tools/Beef.CodeGen.Core/Converters/XmlToYamlConverter.cs
+++ b/tools/Beef.CodeGen.Core/Converters/XmlToYamlConverter.cs
@@ -226,15 +226,6 @@ private static void WriteAttributes(YamlFormatArgs args, ConfigType ct, Configur
if (needsComma)
args.Writer.Write(", ");
- if (!(val.StartsWith("[", StringComparison.OrdinalIgnoreCase) && val.EndsWith("]", StringComparison.OrdinalIgnoreCase)))
- {
- if (val.IndexOfAny(new char[] { ':', '{', '}', '[', ']', ',', '&', '*', '#', '?', '|', '-', '<', '>', '=', '!', '%', '@', '\\', '\"', '\'' }) >= 0)
- val = $"'{val.Replace("'", "''", StringComparison.InvariantCultureIgnoreCase)}'";
-
- if (string.Compare(val, "NULL", StringComparison.InvariantCultureIgnoreCase) == 0)
- val = $"'{val}'";
- }
-
args.HasAttributes = true;
args.Writer.Write($"{jname}: {val}");
diff --git a/tools/Beef.CodeGen.Core/Generators/DatabaseCdcBackgroundServiceCodeGenerator.cs b/tools/Beef.CodeGen.Core/Generators/DatabaseCdcHostedServiceCodeGenerator.cs
similarity index 82%
rename from tools/Beef.CodeGen.Core/Generators/DatabaseCdcBackgroundServiceCodeGenerator.cs
rename to tools/Beef.CodeGen.Core/Generators/DatabaseCdcHostedServiceCodeGenerator.cs
index 8c5c42ccc..6b7b18a48 100644
--- a/tools/Beef.CodeGen.Core/Generators/DatabaseCdcBackgroundServiceCodeGenerator.cs
+++ b/tools/Beef.CodeGen.Core/Generators/DatabaseCdcHostedServiceCodeGenerator.cs
@@ -9,7 +9,7 @@ namespace Beef.CodeGen.Generators
///
/// Represents the Database Change Data Capture (CDC) BackgroundService generator.
///
- public class DatabaseCdcBackgroundServiceCodeGenerator : CodeGeneratorBase
+ public class DatabaseCdcHostedServiceCodeGenerator : CodeGeneratorBase
{
///
///
@@ -17,6 +17,6 @@ public class DatabaseCdcBackgroundServiceCodeGenerator : CodeGeneratorBase
///
protected override IEnumerable SelectGenConfig(CodeGenConfig config)
- => Check.NotNull(config, nameof(config)).Cdc!.Where(x => IsNoOption(x.ExcludeBackgroundService));
+ => Check.NotNull(config, nameof(config)).Cdc!.Where(x => IsNoOption(x.ExcludeHostedService));
}
}
\ No newline at end of file
diff --git a/tools/Beef.CodeGen.Core/Properties/launchSettings.json b/tools/Beef.CodeGen.Core/Properties/launchSettings.json
index 8a682351f..9b74eb15d 100644
--- a/tools/Beef.CodeGen.Core/Properties/launchSettings.json
+++ b/tools/Beef.CodeGen.Core/Properties/launchSettings.json
@@ -2,8 +2,7 @@
"profiles": {
"Beef.CodeGen": {
"commandName": "Project",
- "commandLineArgs": "--GenerateEntityJsonSchema",
- "workingDirectory": "C:\\Users\\eric.sibly\\source\\repos\\Avanade\\Beef\\tools\\Beef.CodeGen.Core"
+ "commandLineArgs": "entity"
}
}
}
\ No newline at end of file
diff --git a/tools/Beef.CodeGen.Core/Schema/codegen.entity.xsd b/tools/Beef.CodeGen.Core/Schema/codegen.entity.xsd
index 1f85964d5..39f82293e 100644
--- a/tools/Beef.CodeGen.Core/Schema/codegen.entity.xsd
+++ b/tools/Beef.CodeGen.Core/Schema/codegen.entity.xsd
@@ -526,6 +526,11 @@
The override for the data entity `Mapper`. Used where the default generated `Mapper` is not applicable.
+
+
+ Indicates whether the `Data` extensions logic should be generated. Defaults to `Entity.DataExtensions`.
+
+ The database stored procedure name used where `Operation.AutoImplement` is `Database`. Defaults to `sp` + `Entity.Name` + `Operation.Name`; e.g. `spPersonCreate`.
@@ -556,6 +561,11 @@
Indicates whether a `System.TransactionScope` should be created and orchestrated at the `Manager`-layer.
+
+
+ Indicates whether the `Manager` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
+
+ The name of the .NET Type that will perform the validation. Defaults to the `Entity.Validator` where not specified explicitly. Only used for `Operation.Type` options `Create` or `Update`.
@@ -590,11 +600,21 @@
Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer.
+
+
+ Indicates whether the `DataSvc` extensions logic should be generated. Defaults to `Entity.ManagerExtensions`.
+
+ Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` or `Entity.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+
+
+ The Event Source. Defaults to `Entity.EventSource`. Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
+
+ The event subject template and corresponding event action pair (separated by a colon). The event subject template defaults to `{AppName}.{Entity.Name}`, plus each of the unique key placeholders comma separated; e.g. `Domain.Entity.{id1},{id2}` (depending on whether `Entity.EventSubjectFormat` is `NameAndKey` or `NameOnly`). The event action defaults to `WebApiOperationType` or `Operation.Type` where not specified. Multiple events can be raised by specifying more than one subject/action pair separated by a semicolon. E.g. `Demo.Person.{id}:Create;Demo.Other.{id}:Update`.
@@ -983,7 +1003,7 @@
- Indicates whether the `Data` extensions logic should be generated.
+ Indicates whether the `Data` extensions logic should be generated. This can be overridden using `Operation.DataExtensions`.
@@ -1096,6 +1116,11 @@
Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to the `CodeGeneration.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc.
+
+
+ The Event Source. Defaults to `Name` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`.
+
+ The default formatting for the Subject when an Event is published. Defaults to `CodeGeneration.EventSubjectFormat`.
@@ -1131,7 +1156,7 @@
- Indicates whether the `DataSvc` extensions logic should be generated.
+ Indicates whether the `DataSvc` extensions logic should be generated. This can be overridden using `Operation.DataSvcExtensions`.
@@ -1153,7 +1178,7 @@
- Indicates whether the `Manager` extensions logic should be generated.
+ Indicates whether the `Manager` extensions logic should be generated. This can be overridden using `Operation.ManagerExtensions`.
@@ -1455,6 +1480,24 @@
Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation. Defaults to `true`. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
+
+
+ The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+
+
+
+
+ The URI kind for the event source URIs. Defaults to `None` (being the event source is not updated).
+
+
+
+
+
+
+
+
+
+ The root for the event Subject name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
@@ -1471,16 +1514,19 @@
+
+
+ The subject path separator. Defaults to `.`. Used only where the subject is automatically inferred.
+
+
- The formatting for the Action when an Event is published. Defaults to `None` (no formatting required)`.
+ The formatting for the Action when an Event is published. Defaults to `None` (no formatting required, i.e. as-is)`.
-
-
diff --git a/tools/Beef.CodeGen.Core/Schema/codegen.table.xsd b/tools/Beef.CodeGen.Core/Schema/codegen.table.xsd
index 2453d3623..b97aafb0a 100644
--- a/tools/Beef.CodeGen.Core/Schema/codegen.table.xsd
+++ b/tools/Beef.CodeGen.Core/Schema/codegen.table.xsd
@@ -754,6 +754,11 @@
The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
+
+
+ The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
+
+ Indicates whether to perform Identifier Mapping (mapping to `GlobalId`) for the primary key. This indicates whether to create a new `GlobalId` property on the _entity_ to house the global mapping identifier to be the reference outside of the specific database realm as a replacement to the existing primary key column(s).
@@ -844,6 +849,23 @@
The .NET database interface name. Defaults to `IDatabase`.
+
+
+ The Event Source. Defaults to `ModelName` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified.
+
+
+
+
+ The default formatting for the Source when an Event is published. Defaults to `CodeGeneration.EventSourceFormat`.
+
+
+
+
+
+
+
+
+ The Event Subject. Defaults to `ModelName`. Note: when used in code-generation the `CodeGeneration.EventSubjectRoot` will be prepended where specified.
@@ -865,9 +887,9 @@
The list of `Column` names that should be included (in addition to the primary key) for a logical delete. Where a column is not specified in this list its corresponding .NET property will be automatically cleared by the `CdcDataOrchestrator` as the data is technically considered as non-existing.
-
+
- The option to exclude the generation of the `BackgroundService` class (`XxxBackgroundService.cs`).
+ The option to exclude the generation of the `CdcHostedService` (background) class (`XxxHostedService.cs`).
@@ -876,6 +898,11 @@
+
+
+ The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking). Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.
+
+ Indicates whether to perform Identifier Mapping (mapping to `GlobalId`) for the primary key. This indicates whether to create a new `GlobalId` property on the _entity_ to house the global mapping identifier to be the reference outside of the specific database realm as a replacement to the existing primary key column(s).
@@ -994,9 +1021,44 @@
The table name for the `Cdc`-IdentifierMapping. Defaults to `spCreateCdcIdentifierMapping` (literal).
+
+
+ The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)
+
+
+
+
+ The URI root for the event source by prepending to all event source URIs. The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s).
+
+
+
+
+ The URI kind for the event source URIs. Defaults to `None` (being the event source is not updated).
+
+
+
+
+
+
+
+
+
+
+
+
+ The default formatting for the Source when an Event is published. Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.
+
+
+
+
+
+
+
+
+
- The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s).
+ The root for the event name by prepending to all event subject names. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be extended within the `Entity`(s).
@@ -1012,14 +1074,12 @@
- The formatting for the Action when an Event is published. Defaults to `None` (no formatting required).
+ The formatting for the Action when an Event is published. Defaults to `None` (no formatting required, i.e. as-is).
-
-
diff --git a/tools/Beef.CodeGen.Core/Schema/database.beef.json b/tools/Beef.CodeGen.Core/Schema/database.beef.json
index 9a9d9d67e..f44c1d84c 100644
--- a/tools/Beef.CodeGen.Core/Schema/database.beef.json
+++ b/tools/Beef.CodeGen.Core/Schema/database.beef.json
@@ -96,10 +96,46 @@
"title": "The table name for the `Cdc`-IdentifierMapping.",
"description": "Defaults to `spCreateCdcIdentifierMapping` (literal)."
},
+ "cdcExcludeColumnsFromETag": {
+ "type": "array",
+ "title": "The default list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking)",
+ "items": [
+ {
+ "type": "string",
+ "uniqueItems": true
+ }
+ ]
+ },
+ "eventSourceRoot": {
+ "type": "string",
+ "title": "The URI root for the event source by prepending to all event source URIs.",
+ "description": "The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s)."
+ },
+ "eventSourceKind": {
+ "type": "string",
+ "title": "The URI kind for the event source URIs.",
+ "description": "Defaults to `None` (being the event source is not updated).",
+ "enum": [
+ "None",
+ "Absolute",
+ "Relative",
+ "RelativeOrAbsolute"
+ ]
+ },
+ "eventSourceFormat": {
+ "type": "string",
+ "title": "The default formatting for the Source when an Event is published.",
+ "description": "Defaults to `NameAndKey` (being the event subject name appended with the corresponding unique key.)`.",
+ "enum": [
+ "NameOnly",
+ "NameAndKey",
+ "NameAndGlobalId"
+ ]
+ },
"eventSubjectRoot": {
"type": "string",
"title": "The root for the event name by prepending to all event subject names.",
- "description": "Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s)."
+ "description": "Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be extended within the `Entity`(s)."
},
"eventSubjectFormat": {
"type": "string",
@@ -113,12 +149,10 @@
"eventActionFormat": {
"type": "string",
"title": "The formatting for the Action when an Event is published.",
- "description": "Defaults to `None` (no formatting required).",
+ "description": "Defaults to `None` (no formatting required, i.e. as-is).",
"enum": [
"None",
- "UpperCase",
- "PastTense",
- "PastTenseUpperCase"
+ "PastTense"
]
},
"jsonSerializer": {
@@ -1037,6 +1071,21 @@
"title": "The .NET database interface name.",
"description": "Defaults to `IDatabase`."
},
+ "eventSource": {
+ "type": "string",
+ "title": "The Event Source.",
+ "description": "Defaults to `ModelName` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified."
+ },
+ "eventSourceFormat": {
+ "type": "string",
+ "title": "The default formatting for the Source when an Event is published.",
+ "description": "Defaults to `CodeGeneration.EventSourceFormat`.",
+ "enum": [
+ "NameOnly",
+ "NameAndKey",
+ "NameAndGlobalId"
+ ]
+ },
"eventSubject": {
"type": "string",
"title": "The Event Subject.",
@@ -1062,14 +1111,25 @@
}
]
},
- "excludeBackgroundService": {
+ "excludeHostedService": {
"type": "string",
- "title": "The option to exclude the generation of the `BackgroundService` class (`XxxBackgroundService.cs`).",
+ "title": "The option to exclude the generation of the `CdcHostedService` (background) class (`XxxHostedService.cs`).",
"enum": [
"No",
"Yes"
]
},
+ "excludeColumnsFromETag": {
+ "type": "array",
+ "title": "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
+ "description": "Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.",
+ "items": [
+ {
+ "type": "string",
+ "uniqueItems": true
+ }
+ ]
+ },
"identifierMapping": {
"type": "boolean",
"title": "Indicates whether to perform Identifier Mapping (mapping to `GlobalId`) for the primary key.",
@@ -1207,6 +1267,17 @@
}
]
},
+ "excludeColumnsFromETag": {
+ "type": "array",
+ "title": "The list of `Column` names that should be excluded from the generated ETag (used for the likes of duplicate send tracking).",
+ "description": "Defaults to `CodeGeneration.CdcExcludeColumnsFromETag`.",
+ "items": [
+ {
+ "type": "string",
+ "uniqueItems": true
+ }
+ ]
+ },
"identifierMapping": {
"type": "boolean",
"title": "Indicates whether to perform Identifier Mapping (mapping to `GlobalId`) for the primary key.",
diff --git a/tools/Beef.CodeGen.Core/Schema/entity.beef.json b/tools/Beef.CodeGen.Core/Schema/entity.beef.json
index f73b0e1b4..f954098c2 100644
--- a/tools/Beef.CodeGen.Core/Schema/entity.beef.json
+++ b/tools/Beef.CodeGen.Core/Schema/entity.beef.json
@@ -166,6 +166,22 @@
"title": "Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation.",
"description": "Defaults to `true`. Used to enable the sending of messages to the likes of EventHub, Service Broker, SignalR, etc. This can be overridden within the `Entity`(s)."
},
+ "eventSourceRoot": {
+ "type": "string",
+ "title": "The URI root for the event source by prepending to all event source URIs.",
+ "description": "The event source is only updated where an `EventSourceKind` is not `None`. This can be extended within the `Entity`(s)."
+ },
+ "eventSourceKind": {
+ "type": "string",
+ "title": "The URI kind for the event source URIs.",
+ "description": "Defaults to `None` (being the event source is not updated).",
+ "enum": [
+ "None",
+ "Absolute",
+ "Relative",
+ "RelativeOrAbsolute"
+ ]
+ },
"eventSubjectRoot": {
"type": "string",
"title": "The root for the event Subject name by prepending to all event subject names.",
@@ -180,15 +196,18 @@
"NameAndKey"
]
},
+ "eventSubjectSeparator": {
+ "type": "string",
+ "title": "The subject path separator.",
+ "description": "Defaults to `.`. Used only where the subject is automatically inferred."
+ },
"eventActionFormat": {
"type": "string",
"title": "The formatting for the Action when an Event is published.",
- "description": "Defaults to `None` (no formatting required)`.",
+ "description": "Defaults to `None` (no formatting required, i.e. as-is)`.",
"enum": [
"None",
- "UpperCase",
- "PastTense",
- "PastTenseUpperCase"
+ "PastTense"
]
},
"eventTransaction": {
@@ -454,7 +473,8 @@
},
"dataExtensions": {
"type": "boolean",
- "title": "Indicates whether the `Data` extensions logic should be generated."
+ "title": "Indicates whether the `Data` extensions logic should be generated.",
+ "description": "This can be overridden using `Operation.DataExtensions`."
},
"databaseName": {
"type": "string",
@@ -558,6 +578,11 @@
"title": "Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation.",
"description": "Defaults to the `CodeGeneration.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc."
},
+ "eventSource": {
+ "type": "string",
+ "title": "The Event Source.",
+ "description": "Defaults to `Name` (as lowercase). Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`."
+ },
"eventSubjectFormat": {
"type": "string",
"title": "The default formatting for the Subject when an Event is published.",
@@ -595,7 +620,8 @@
},
"dataSvcExtensions": {
"type": "boolean",
- "title": "Indicates whether the `DataSvc` extensions logic should be generated."
+ "title": "Indicates whether the `DataSvc` extensions logic should be generated.",
+ "description": "This can be overridden using `Operation.DataSvcExtensions`."
},
"managerCtor": {
"type": "string",
@@ -620,7 +646,8 @@
},
"managerExtensions": {
"type": "boolean",
- "title": "Indicates whether the `Manager` extensions logic should be generated."
+ "title": "Indicates whether the `Manager` extensions logic should be generated.",
+ "description": "This can be overridden using `Operation.ManagerExtensions`."
},
"validator": {
"type": "string",
@@ -1134,6 +1161,11 @@
"title": "The override for the data entity `Mapper`.",
"description": "Used where the default generated `Mapper` is not applicable."
},
+ "dataExtensions": {
+ "type": "boolean",
+ "title": "Indicates whether the `Data` extensions logic should be generated.",
+ "description": "Defaults to `Entity.DataExtensions`."
+ },
"databaseStoredProc": {
"type": "string",
"title": "The database stored procedure name used where `Operation.AutoImplement` is `Database`.",
@@ -1162,6 +1194,11 @@
"type": "boolean",
"title": "Indicates whether a `System.TransactionScope` should be created and orchestrated at the `Manager`-layer."
},
+ "managerExtensions": {
+ "type": "boolean",
+ "title": "Indicates whether the `Manager` extensions logic should be generated.",
+ "description": "Defaults to `Entity.ManagerExtensions`."
+ },
"validator": {
"type": "string",
"title": "The name of the .NET Type that will perform the validation.",
@@ -1192,11 +1229,21 @@
"type": "boolean",
"title": "Indicates whether a `System.TransactionScope` should be created and orchestrated at the `DataSvc`-layer."
},
+ "dataSvcExtensions": {
+ "type": "boolean",
+ "title": "Indicates whether the `DataSvc` extensions logic should be generated.",
+ "description": "Defaults to `Entity.ManagerExtensions`."
+ },
"eventPublish": {
"type": "boolean",
"title": "Indicates whether to add logic to publish an event on the successful completion of the `DataSvc` layer invocation for a `Create`, `Update` or `Delete` operation.",
"description": "Defaults to the `CodeGeneration.EventPublish` or `Entity.EventPublish` configuration property (inherits) where not specified. Used to enable the sending of messages to the likes of EventGrid, Service Broker, SignalR, etc."
},
+ "eventSource": {
+ "type": "string",
+ "title": "The Event Source.",
+ "description": "Defaults to `Entity.EventSource`. Note: when used in code-generation the `CodeGeneration.EventSourceRoot` will be prepended where specified. To include the entity id/key include a `{$key}` placeholder (`Create`, `Update` or `Delete` operation only); for example: `person/{$key}`. This can be overridden for the `Entity`."
+ },
"eventSubject": {
"type": "string",
"title": "The event subject template and corresponding event action pair (separated by a colon).",
diff --git a/tools/Beef.CodeGen.Core/Scripts/DatabaseCdc.xml b/tools/Beef.CodeGen.Core/Scripts/DatabaseCdc.xml
index 17da0acc7..7e0ad56ac 100644
--- a/tools/Beef.CodeGen.Core/Scripts/DatabaseCdc.xml
+++ b/tools/Beef.CodeGen.Core/Scripts/DatabaseCdc.xml
@@ -4,8 +4,9 @@
-
+
+
diff --git a/tools/Beef.CodeGen.Core/Templates/DbCdcData_cs.hb b/tools/Beef.CodeGen.Core/Templates/DbCdcData_cs.hb
index 23409c6d6..cc479a698 100644
--- a/tools/Beef.CodeGen.Core/Templates/DbCdcData_cs.hb
+++ b/tools/Beef.CodeGen.Core/Templates/DbCdcData_cs.hb
@@ -103,20 +103,32 @@ namespace {{Root.NamespaceCdc}}.Data
}
///
- /// Gets the format.
+ /// Gets the (to be further formatted as per ).
///
- protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.{{EventSubjectFormat}};
+ protected override string EventSubject => "{{#ifval Root.EventSubjectRoot}}{{Root.EventSubjectRoot}}.{{/ifval}}{{EventSubject}}";
///
- /// Gets the (to be further formatted as per ).
+ /// Gets the .
///
- protected override string EventSubject => "{{#ifval Root.EventSubjectRoot}}{{Root.EventSubjectRoot}}.{{/ifval}}{{EventSubject}}";
+ protected override EventSubjectFormat EventSubjectFormat => EventSubjectFormat.{{EventSubjectFormat}};
///
- /// Gets the .
+ /// Gets the .
///
protected override EventActionFormat EventActionFormat => EventActionFormat.{{Root.EventActionFormat}};
+{{#ifne Root.EventSourceKind 'None'}}
+ ///
+ /// Gets the .
+ ///
+ protected override Uri? EventSource => new Uri("{{EventSourceUri}}", UriKind.{{Root.EventSourceKind}});
+
+ ///
+ /// Gets the .
+ ///
+ protected override EventSourceFormat EventSourceFormat { get; } = EventSourceFormat.{{EventSourceFormat}};
+
+{{/ifne}}
{{#ifne ExcludePropertiesFromETag.Count 0}}
///
/// Gets the list of property names that should be excluded from the serialized JSON generation.
diff --git a/tools/Beef.CodeGen.Core/Templates/DbCdcEntity_cs.hb b/tools/Beef.CodeGen.Core/Templates/DbCdcEntity_cs.hb
index 56269aeba..444cb06b9 100644
--- a/tools/Beef.CodeGen.Core/Templates/DbCdcEntity_cs.hb
+++ b/tools/Beef.CodeGen.Core/Templates/DbCdcEntity_cs.hb
@@ -170,7 +170,7 @@ namespace {{Root.NamespaceCdc}}.Entities
public async Task LinkIdentifierMappingsAsync(CdcValueIdentifierMappingCollection coll, IStringIdentifierGenerator idGen)
{
{{#if IdentifierMapping}}
- coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "{{Schema}}", Table = "{{Name}}", Key = this.CreateFormattedKey(), GlobalId = await idGen.GenerateIdentifierAsync<{{ModelName}}Cdc>().ConfigureAwait(false) });
+ coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "{{Schema}}", Table = "{{Name}}", Key = this.CreateIdentifierMappingKey(), GlobalId = await idGen.GenerateIdentifierAsync<{{ModelName}}Cdc>().ConfigureAwait(false) });
{{/if}}
{{#each SelectedEntityColumns}}
{{#ifval IdentifierMappingParent}}
@@ -316,7 +316,7 @@ namespace {{Root.NamespaceCdc}}.Entities
public async Task LinkIdentifierMappingsAsync(CdcValueIdentifierMappingCollection coll, IStringIdentifierGenerator idGen)
{
{{#if IdentifierMapping}}
- coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "{{Schema}}", Table = "{{TableName}}", Key = this.CreateFormattedKey(), GlobalId = await idGen.GenerateIdentifierAsync<{{ModelName}}Cdc>().ConfigureAwait(false) });
+ coll.AddAsync(GlobalId == default, async () => new CdcValueIdentifierMapping { Value = this, Property = nameof(GlobalId), Schema = "{{Schema}}", Table = "{{TableName}}", Key = this.CreateIdentifierMappingKey(), GlobalId = await idGen.GenerateIdentifierAsync<{{ModelName}}Cdc>().ConfigureAwait(false) });
{{/if}}
{{#each Columns}}
{{#ifval IdentifierMappingParent}}
diff --git a/tools/Beef.CodeGen.Core/Templates/DbCdcHostedExtensions_cs.hb b/tools/Beef.CodeGen.Core/Templates/DbCdcHostedExtensions_cs.hb
new file mode 100644
index 000000000..8bc7945a2
--- /dev/null
+++ b/tools/Beef.CodeGen.Core/Templates/DbCdcHostedExtensions_cs.hb
@@ -0,0 +1,39 @@
+{{! Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/Beef }}
+/*
+ * This file is automatically generated; any changes will be lost.
+ */
+
+#nullable enable
+#pragma warning disable
+
+using Beef.Data.Database.Cdc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace {{Root.NamespaceCdc}}.Services
+{
+ ///
+ /// Provides the generated extensions.
+ ///
+ public static class CdcHostedServiceExtensions
+ {
+ ///
+ /// Adds the generated CDC hosted services.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ public static IServiceCollection AddGeneratedCdcHostedServices(this IServiceCollection services, IConfiguration config)
+ {
+{{#each Cdc}}
+ {{#ifne ExcludeHostedService 'Yes'}}
+ services.AddCdcHostedService<{{ModelName}}CdcHostedService>(config);
+ {{/ifne}}
+{{/each}}
+ return services;
+ }
+ }
+}
+
+#pragma warning restore
+#nullable restore
\ No newline at end of file
diff --git a/tools/Beef.CodeGen.Core/Templates/DbCdcBackgroundService_cs.hb b/tools/Beef.CodeGen.Core/Templates/DbCdcHostedService_cs.hb
similarity index 66%
rename from tools/Beef.CodeGen.Core/Templates/DbCdcBackgroundService_cs.hb
rename to tools/Beef.CodeGen.Core/Templates/DbCdcHostedService_cs.hb
index b5001d6f8..26f26b20f 100644
--- a/tools/Beef.CodeGen.Core/Templates/DbCdcBackgroundService_cs.hb
+++ b/tools/Beef.CodeGen.Core/Templates/DbCdcHostedService_cs.hb
@@ -16,17 +16,17 @@ using {{Root.NamespaceCdc}}.Data;
namespace {{Root.NamespaceCdc}}.Services
{
///
- /// Provides the CDC background service for database object '{{Schema}}.{{Name}}'.
+ /// Provides the capabilities for database object '{{Schema}}.{{Name}}'.
///
- public partial class {{ModelName}}CdcBackgroundService : CdcBackgroundService
+ public partial class {{ModelName}}CdcHostedService : CdcHostedService
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The .
/// The .
/// The .
- public {{ModelName}}CdcBackgroundService(IServiceProvider serviceProvider, IConfiguration config, ILogger<{{ModelName}}CdcBackgroundService> logger) : base(serviceProvider, config, logger) { }
+ public {{ModelName}}CdcHostedService(IServiceProvider serviceProvider, IConfiguration config, ILogger<{{ModelName}}CdcHostedService> logger) : base(serviceProvider, config, logger) { }
}
}
diff --git a/tools/Beef.CodeGen.Core/Templates/DbCdcOutboxTableCreate_sql.hb b/tools/Beef.CodeGen.Core/Templates/DbCdcOutboxTableCreate_sql.hb
index 31abb77f3..0cdc54faa 100644
--- a/tools/Beef.CodeGen.Core/Templates/DbCdcOutboxTableCreate_sql.hb
+++ b/tools/Beef.CodeGen.Core/Templates/DbCdcOutboxTableCreate_sql.hb
@@ -6,11 +6,11 @@ CREATE TABLE [{{CdcSchema}}].[{{OutboxTableName}}] (
[OutboxId] INT IDENTITY (1, 1) NOT NULL PRIMARY KEY CLUSTERED ([OutboxId] ASC),
[CreatedDate] DATETIME NOT NULL,
- [{{pascal Name}}MinLsn] BINARY(10) NOT NULL, -- Primary table: {{Schema}}.{{Name}}
- [{{pascal Name}}MaxLsn] BINARY(10) NOT NULL,
+ [{{pascal Name}}MinLsn] BINARY(10) NULL, -- Primary table: {{Schema}}.{{Name}}
+ [{{pascal Name}}MaxLsn] BINARY(10) NULL,
{{#each CdcJoins}}
- [{{pascal Name}}MinLsn] BINARY(10) NOT NULL, -- Related table: {{Schema}}.{{TableName}}
- [{{pascal Name}}MaxLsn] BINARY(10) NOT NULL,
+ [{{pascal Name}}MinLsn] BINARY(10) NULL, -- Related table: {{Schema}}.{{TableName}}
+ [{{pascal Name}}MaxLsn] BINARY(10) NULL,
{{/each}}
[IsComplete] BIT NOT NULL,
[CompletedDate] DATETIME NULL,
diff --git a/tools/Beef.CodeGen.Core/Templates/EntityDataSvc_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityDataSvc_cs.hbs
index 52fc13475..0fc3cf960 100644
--- a/tools/Beef.CodeGen.Core/Templates/EntityDataSvc_cs.hbs
+++ b/tools/Beef.CodeGen.Core/Templates/EntityDataSvc_cs.hbs
@@ -48,11 +48,13 @@ namespace {{Root.NamespaceBusiness}}.DataSvc
{{/if}}
{{/each}}
-{{#if DataSvcExtensions}}
+{{#if HasDataSvcExtensions}}
#region Extensions
{{#each DataSvcAutoOperations}}
+ {{#if DataSvcExtensions}}
private Func<{{#if HasReturnValue}}{{OperationReturnType}}, {{/if}}{{#each ValueLessDataParameters}}{{{ParameterType}}}, {{/each}}Task>? {{PrivateName}}OnAfterAsync;
+ {{/if}}
{{/each}}
#endregion
@@ -104,13 +106,13 @@ namespace {{Root.NamespaceBusiness}}.DataSvc
{{/ifeq}}
{{/if}}
{{#if HasReturnValue}}var __result = {{/if}}await _data.{{Name}}Async({{#each DataParameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ArgumentName}}{{/if}}{{/each}}).ConfigureAwait(false);
- {{#if Parent.DataSvcExtensions}}
- if ({{PrivateName}}OnAfterAsync != null) await {{PrivateName}}OnAfterAsync({{#if HasReturnValue}}__result{{/if}}{{#each ValueLessDataParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{{ArgumentName}}}{{/each}}).ConfigureAwait(false);
+ {{#if DataSvcExtensions}}
+ await ({{PrivateName}}OnAfterAsync?.Invoke({{#if HasReturnValue}}__result{{/if}}{{#each ValueLessDataParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{{ArgumentName}}}{{/each}}) ?? Task.CompletedTask).ConfigureAwait(false);
{{/if}}
{{#if EventPublish}}
{{#ifeq Events.Count 1}}
{{#each Events}}
- await _evtPub.Publish{{#if ../HasReturnValue}}Value{{/if}}({{#if ../HasReturnValue}}__result, {{/if}}$"{{Subject}}", "{{Action}}"{{#each ../ValueLessDataParameters}}, {{ArgumentName}}{{/each}}).SendAsync().ConfigureAwait(false);
+ await _evtPub.Publish{{#ifval Value}}Value{{/ifval}}({{#ifval Value}}{{Value}}, {{/ifval}}{{#ifval Source}}new Uri($"{{Source}}", UriKind.{{../../Root.EventSourceKind}}), {{/ifval}}$"{{Subject}}", "{{Action}}"{{#each ../ValueLessDataParameters}}, {{ArgumentName}}{{/each}}).SendAsync().ConfigureAwait(false);
{{/each}}
{{else}}
{{#ifeq Events.Count 0}}
@@ -118,7 +120,7 @@ namespace {{Root.NamespaceBusiness}}.DataSvc
{{else}}
await _evtPub.Publish(
{{#each Events}}
- _evtPub.Create{{#if ../HasReturnValue}}Value{{/if}}Event({{#if ../HasReturnValue}}__result, {{/if}}$"{{Subject}}", "{{Action}}"{{#each ../ValueLessDataParameters}}, {{ArgumentName}}{{/each}}){{#if @last}}).SendAsync().ConfigureAwait(false);{{else}},{{/if}}
+ _evtPub.Create{{#ifval Value}}Value{{/ifval}}Event({{#ifval Value}}{{Value}}, {{/ifval}}{{#ifval Source}}new Uri($"{{Source}}", UriKind.{{../../Root.EventSourceKind}}), {{/ifval}}$"{{Subject}}", "{{Action}}"{{#each ../ValueLessDataParameters}}, {{ArgumentName}}{{/each}}){{#if @last}}).SendAsync().ConfigureAwait(false);{{else}},{{/if}}
{{#if @last}}
{{/if}}
@@ -130,11 +132,12 @@ namespace {{Root.NamespaceBusiness}}.DataSvc
{{#ifeq Type 'Delete'}}
_cache.Remove<{{Parent.Name}}>(new UniqueKey({{#each ValueLessDataParameters}}{{#unless @first}}, {{/unless}}{{ArgumentName}}{{/each}}));
{{else}}
- _cache.SetValue({{#ifeq Type 'Get'}}__key{{else}}(__result as IUniqueKey).UniqueKey{{/ifeq}}, __result);
+ return _cache.SetAndReturnValue({{#ifeq Type 'Get'}}__key, {{/ifeq}}__result);
{{/ifeq}}
- {{/if}}
- {{#if HasReturnValue}}
+ {{else}}
+ {{#if HasReturnValue}}
return __result;
+ {{/if}}
{{/if}}
}{{#if DataSvcTransaction}}, new BusinessInvokerArgs { IncludeTransactionScope = true }{{/if}});
}
diff --git a/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs
index d763619c5..dd1124320 100644
--- a/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs
+++ b/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs
@@ -74,7 +74,7 @@ namespace {{Root.NamespaceBusiness}}.Data
{{/if}}
{{/each}}
{{#if DataExtensionsRequired}}
- {{#if DataExtensions}}
+ {{#if HasDataExtensions}}
#region Extensions
{{/if}}
@@ -100,7 +100,7 @@ namespace {{Root.NamespaceBusiness}}.Data
private Func, {{#each CoreDataParameters}}{{{ParameterType}}}, {{/each}}{{DataArgs.Type}}, Soc.IBoundClient<{{Parent.ODataModel}}>>? {{PrivateName}}OnQuery;
{{/ifeq}}
{{/ifeq}}
- {{#if Parent.DataExtensions}}
+ {{#if DataExtensions}}
{{#ifne AutoImplement 'None'}}
private Func<{{#each PagingLessDataParameters}}{{{ParameterType}}}, {{/each}}{{DataArgs.Type}}, Task>? {{PrivateName}}OnBeforeAsync;
private Func<{{#if HasReturnValue}}{{OperationReturnType}}, {{/if}}{{#each CoreDataParameters}}{{{ParameterType}}}, {{/each}}Task>? {{PrivateName}}OnAfterAsync;
@@ -109,7 +109,7 @@ namespace {{Root.NamespaceBusiness}}.Data
{{/if}}
{{/each}}
- {{#if DataExtensions}}
+ {{#if HasDataExtensions}}
#endregion
{{/if}}
@@ -146,7 +146,7 @@ namespace {{Root.NamespaceBusiness}}.Data
{{/if}}
public {{{OperationTaskReturnType}}} {{Name}}Async({{#each DataParameters}}{{#unless @first}}, {{/unless}}{{{ParameterType}}} {{ArgumentName}}{{/each}})
{{#ifeq AutoImplement 'None'}}
- {{#if Parent.DataExtensions}}
+ {{#if DataExtensions}}
=> DataInvoker.Current.InvokeAsync(this, () => {{Name}}OnImplementationAsync({{#each DataParameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ArgumentName}}{{/if}}{{/each}}), new BusinessInvokerArgs { ExceptionHandler = {{PrivateName}}OnException{{#if DataTransaction}}, IncludeTransactionScope = true{{/if}} });
{{else}}
=> DataInvoker.Current.InvokeAsync(this, () => {{Name}}OnImplementationAsync({{#each DataParameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ArgumentName}}{{/if}}{{/each}}){{#if DataTransaction}}, new BusinessInvokerArgs { IncludeTransactionScope = true }{{/if}});
@@ -158,7 +158,7 @@ namespace {{Root.NamespaceBusiness}}.Data
{{#ifeq Type 'GetColl'}}
{{OperationReturnType}} __result = new {{OperationReturnType}}({{#if Paging}}paging{{/if}});
{{else}}
- {{#if Parent.DataExtensions}}
+ {{#if DataExtensions}}
{{#if HasReturnValue}}
{{OperationReturnType}} __result;
{{/if}}
@@ -176,39 +176,39 @@ namespace {{Root.NamespaceBusiness}}.Data
{{#ifeq AutoImplement 'OData'}}
var __dataArgs = {{DataEntityMapper}}.Default.CreateArgs({{#if Paging}}__result.Paging!{{/if}}{{#ifval ODataCollectionName}}{{#if Paging}}, {{/if}}"{{ODataCollectionName}}"{{/ifval}});
{{/ifeq}}
- {{#if Parent.DataExtensions}}
- if ({{PrivateName}}OnBeforeAsync != null) await {{PrivateName}}OnBeforeAsync({{#each PagingLessDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs).ConfigureAwait(false);
+ {{#if DataExtensions}}
+ await ({{PrivateName}}OnBeforeAsync?.Invoke({{#each PagingLessDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs) ?? Task.CompletedTask).ConfigureAwait(false);
{{/if}}
{{#ifeq AutoImplement 'Database'}}
{{#ifeq Type 'GetColl'}}
__result.Result = await {{DataArgs.Name}}.Query(__dataArgs, p => {{PrivateName}}OnQuery?.Invoke(p, {{#each CoreDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs)).SelectQueryAsync<{{Parent.EntityCollectionName}}>().ConfigureAwait(false);
{{else}}
- {{#if Parent.DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
+ {{#if DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
{{/ifeq}}
{{/ifeq}}
{{#ifeq AutoImplement 'EntityFramework'}}
{{#ifeq Type 'GetColl'}}
__result.Result = {{DataArgs.Name}}.Query(__dataArgs, q => {{PrivateName}}OnQuery?.Invoke(q, {{#each CoreDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs) ?? q).SelectQuery<{{BaseReturnType}}Collection>();
{{else}}
- {{#if Parent.DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
+ {{#if DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
{{/ifeq}}
{{/ifeq}}
{{#ifeq AutoImplement 'Cosmos'}}
{{#ifeq Type 'GetColl'}}
__result.Result = {{DataArgs.Name}}.{{#if CosmosValueContainer}}Value{{/if}}Container(__dataArgs).Query(q => {{PrivateName}}OnQuery?.Invoke(q, {{#each CoreDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs) ?? q).SelectQuery<{{BaseReturnType}}Collection>();
{{else}}
- {{#if Parent.DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{#if CosmosValueContainer}}Value{{/if}}Container(__dataArgs).{{Type}}Async({{#each PagingLessDataParameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
+ {{#if DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{#if CosmosValueContainer}}Value{{/if}}Container(__dataArgs).{{Type}}Async({{#each PagingLessDataParameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
{{/ifeq}}
{{/ifeq}}
{{#ifeq AutoImplement 'OData'}}
{{#ifeq Type 'GetColl'}}
__result.Result = {{DataArgs.Name}}.Query(__dataArgs, q => {{PrivateName}}OnQuery?.Invoke(q, {{#each CoreDataParameters}}{{{ArgumentName}}}, {{/each}}__dataArgs) ?? q).SelectQuery<{{BaseReturnType}}Collection>();
{{else}}
- {{#if Parent.DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
+ {{#if DataExtensions}}{{#if HasReturnValue}}__result = {{/if}}{{else}}{{#if HasReturnValue}}return {{/if}}{{/if}}await {{DataArgs.Name}}.{{Type}}Async(__dataArgs{{#each PagingLessDataParameters}}, {{#if IsValueArg}}Check.NotNull(value, nameof(value)){{else}}{{ParameterConverted}}{{/if}}{{/each}}).ConfigureAwait(false);
{{/ifeq}}
{{/ifeq}}
- {{#if Parent.DataExtensions}}
- if ({{PrivateName}}OnAfterAsync != null) await {{PrivateName}}OnAfterAsync({{#if HasReturnValue}}__result{{/if}}{{#each CoreDataParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{ArgumentName}}{{/each}}).ConfigureAwait(false);
+ {{#if DataExtensions}}
+ await ({{PrivateName}}OnAfterAsync?.Invoke({{#if HasReturnValue}}__result{{/if}}{{#each CoreDataParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{ArgumentName}}{{/each}}) ?? Task.CompletedTask).ConfigureAwait(false);
{{#if HasReturnValue}}
return __result;
{{/if}}
diff --git a/tools/Beef.CodeGen.Core/Templates/EntityManager_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityManager_cs.hbs
index 55fb5e502..b4c55ee7d 100644
--- a/tools/Beef.CodeGen.Core/Templates/EntityManager_cs.hbs
+++ b/tools/Beef.CodeGen.Core/Templates/EntityManager_cs.hbs
@@ -44,17 +44,19 @@ namespace {{Root.NamespaceBusiness}}
{{/if}}
{{/each}}
-{{#if ManagerExtensions}}
+{{#if HasManagerExtensions}}
#region Extensions
{{#each ManagerAutoOperations}}
- {{#unless SingleValidateParameters}}
+ {{#if ManagerExtensions}}
+ {{#unless SingleValidateParameters}}
private Func<{{#each Parameters}}{{{ParameterType}}}, {{/each}}Task>? {{PrivateName}}OnPreValidateAsync;
private Action? {{PrivateName}}OnValidate;
- {{/unless}}
+ {{/unless}}
private Func<{{#each Parameters}}{{{ParameterType}}}, {{/each}}Task>? {{PrivateName}}OnBeforeAsync;
private Func<{{#if HasReturnValue}}{{OperationReturnType}}, {{/if}}{{#each ValueLessParameters}}{{{ParameterType}}}, {{/each}}Task>? {{PrivateName}}OnAfterAsync;
+ {{/if}}
{{/each}}
#endregion
@@ -127,9 +129,9 @@ namespace {{Root.NamespaceBusiness}}
{{#ifne CleanerParameters.Count 0}}
Cleaner.CleanUp({{#each CleanerParameters}}{{#unless @first}}, {{/unless}}{{ArgumentName}}{{/each}});
{{/ifne}}
- {{#if Parent.ManagerExtensions}}
+ {{#if ManagerExtensions}}
{{#unless SingleValdiateParameters}}
- if ({{PrivateName}}OnPreValidateAsync != null) await {{PrivateName}}OnPreValidateAsync({{#each Parameters}}{{#unless @first}}, {{/unless}}{{{ArgumentName}}}{{/each}}).ConfigureAwait(false);
+ await ({{PrivateName}}OnPreValidateAsync?.Invoke({{#each Parameters}}{{#unless @first}}, {{/unless}}{{{ArgumentName}}}{{/each}}) ?? Task.CompletedTask).ConfigureAwait(false);
{{/unless}}
{{/if}}
@@ -142,7 +144,7 @@ namespace {{Root.NamespaceBusiness}}
{{#each ValidateParameters}}
.Add({{ArgumentName}}.Validate(nameof({{ArgumentName}})){{#if IsMandatory}}.Mandatory(){{/if}}{{#ifval Validator}}.Entity().With<{{IValidator}}>(){{/ifval}}{{#ifval ValidatorCode}}.{{ValidatorCode}}{{/ifval}})
{{/each}}
- {{#if Parent.ManagerExtensions}}
+ {{#if ManagerExtensions}}
{{#unless SingleValidateParameters}}
.Additional((__mv) => {{PrivateName}}OnValidate?.Invoke(__mv{{#each Parameters}}, {{ArgumentName}}{{/each}}))
{{/unless}}
@@ -150,10 +152,10 @@ namespace {{Root.NamespaceBusiness}}
.RunAsync().ConfigureAwait(false)).ThrowOnError();
{{/if}}
- {{#if Parent.ManagerExtensions}}
- if ({{PrivateName}}OnBeforeAsync != null) await {{PrivateName}}OnBeforeAsync({{#each Parameters}}{{#unless @first}}, {{/unless}}{{{ArgumentName}}}{{/each}}).ConfigureAwait(false);
+ {{#if ManagerExtensions}}
+ await ({{PrivateName}}OnBeforeAsync?.Invoke({{#each Parameters}}{{#unless @first}}, {{/unless}}{{{ArgumentName}}}{{/each}}) ?? Task.CompletedTask).ConfigureAwait(false);
{{#if HasReturnValue}}var __result = {{/if}}await _dataService.{{Name}}Async({{#each DataParameters}}{{#unless @first}}, {{/unless}}{{{ArgumentName}}}{{/each}}).ConfigureAwait(false);
- if ({{PrivateName}}OnAfterAsync != null) await {{PrivateName}}OnAfterAsync({{#if HasReturnValue}}__result{{/if}}{{#each ValueLessParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{{ArgumentName}}}{{/each}}).ConfigureAwait(false);
+ await ({{PrivateName}}OnAfterAsync?.Invoke({{#if HasReturnValue}}__result{{/if}}{{#each ValueLessParameters}}{{#if @first}}{{#if Parent.HasReturnValue}}, {{/if}}{{else}}, {{/if}}{{{ArgumentName}}}{{/each}}) ?? Task.CompletedTask).ConfigureAwait(false);
{{#if HasReturnValue}}
return Cleaner.Clean(__result);
{{/if}}
diff --git a/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgentArgs_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgentArgs_cs.hbs
index 0afcc7c48..495373982 100644
--- a/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgentArgs_cs.hbs
+++ b/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgentArgs_cs.hbs
@@ -8,6 +8,7 @@
using Beef.WebApi;
using System;
using System.Net.Http;
+using System.Threading.Tasks;
namespace {{Root.NamespaceCommon}}.Agents
{
@@ -26,7 +27,8 @@ namespace {{Root.NamespaceCommon}}.Agents
///
/// The .
/// The optional action.
- public {{AppName}}WebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null) : base(httpClient, beforeRequest) { }
+ /// The optional asynchronous function.
+ public {{AppName}}WebApiAgentArgs(HttpClient httpClient, Action? beforeRequest = null, Func? beforeRequestAsync = null) : base(httpClient, beforeRequest, beforeRequestAsync) { }
}
}
diff --git a/tools/Beef.CodeGen.Core/Templates/ServiceCollectionExtensionsValidation_cs.hbs b/tools/Beef.CodeGen.Core/Templates/ServiceCollectionExtensionsValidation_cs.hbs
index 3697da0ea..5d7bb9fca 100644
--- a/tools/Beef.CodeGen.Core/Templates/ServiceCollectionExtensionsValidation_cs.hbs
+++ b/tools/Beef.CodeGen.Core/Templates/ServiceCollectionExtensionsValidation_cs.hbs
@@ -10,7 +10,9 @@ using Microsoft.Extensions.DependencyInjection;
{{#ifeq EntityUsing 'Business' 'All'}}
using {{Root.NamespaceBusiness}}.Entities;
{{/ifeq}}
+{{#ifne Validators.Count 0}}
using {{Root.NamespaceBusiness}}.Validation;
+{{/ifne}}
{{#ifeq EntityUsing 'Common' 'All'}}
using {{Root.NamespaceCommon}}.Entities;
{{/ifeq}}
@@ -32,6 +34,9 @@ namespace {{Root.NamespaceBusiness}}
{{#each Validators}}
{{#if @first}}return services{{else}} {{/if}}.AddScoped<{{Type}}, {{Name}}>(){{#if @last}};{{/if}}
{{/each}}
+{{#ifeq Validators.Count 0}}
+ return services;
+{{/ifeq}}
}
}
}
diff --git a/tools/Beef.Database.Core/Beef.Database.Core.csproj b/tools/Beef.Database.Core/Beef.Database.Core.csproj
index 695593729..a9b3974ef 100644
--- a/tools/Beef.Database.Core/Beef.Database.Core.csproj
+++ b/tools/Beef.Database.Core/Beef.Database.Core.csproj
@@ -5,7 +5,7 @@
Exe
- 4.1.4
+ 4.1.5Beef DevelopersAvanadefalse
diff --git a/tools/Beef.Database.Core/CHANGELOG.md b/tools/Beef.Database.Core/CHANGELOG.md
index 8fc0a3dfb..cdb422814 100644
--- a/tools/Beef.Database.Core/CHANGELOG.md
+++ b/tools/Beef.Database.Core/CHANGELOG.md
@@ -4,6 +4,7 @@ Represents the **NuGet** versions.
## v4.1.5
- *Enhancement:* Added additional statistics information to console output.
+- *Enhancement:* Will strip out `bin/debug` and `bin/release` folders from default directory path to find the output directory; meaning the path does not need to be explicity set when running/debugging from Visual Studio.
## v4.1.4
- *Enhancement:* Updated all dependent NuGet packages to their latest respective version.
diff --git a/tools/Beef.Database.Core/DatabaseConsoleWrapper.cs b/tools/Beef.Database.Core/DatabaseConsoleWrapper.cs
index 206e03f97..294d07304 100644
--- a/tools/Beef.Database.Core/DatabaseConsoleWrapper.cs
+++ b/tools/Beef.Database.Core/DatabaseConsoleWrapper.cs
@@ -4,6 +4,7 @@
using McMaster.Extensions.CommandLineUtils;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
@@ -18,7 +19,8 @@ public class DatabaseConsoleWrapper
{
private readonly List _assemblies = new List();
private readonly List _schemaOrder = new List();
- private string _outDir = "./..";
+ private string _exeDir;
+ private string _outDir;
private string _script = "Database.xml";
private DatabaseExecutorCommand _supports = DatabaseExecutorCommand.All;
@@ -26,7 +28,7 @@ public class DatabaseConsoleWrapper
/// Gets the command line template.
///
public static string CommandLineTemplate { get; set; }
- = "{{Command}} \"{{ConnectionString}}\" {{Assembly}} -c {{ConfigFile}} -s {{Script}} -o {{OutDir}} -su {{Supported}} -p Company={{Company}} -p AppName={{AppName}} -p AppDir={{AppName}}";
+ = "{{Command}} \"{{ConnectionString}}\" {{Assembly}} -c \"{{ConfigFile}}\" -s {{Script}} -o \"{{OutDir}}\" -su {{Supported}} -p Company={{Company}} -p AppName={{AppName}} -p AppDir={{AppName}}";
///
/// Gets the command line assembly portion template.
@@ -66,6 +68,9 @@ private DatabaseConsoleWrapper(string connectionString, string company, string a
}
OverrideConnectionString();
+
+ _exeDir = CodeGenFileManager.GetExeDirectory();
+ _outDir = new DirectoryInfo(_exeDir).Parent.FullName;
}
///
@@ -178,7 +183,7 @@ public async Task RunAsync(string[] args)
throw new CommandParsingException(app, $"Command '{cmd.ParsedValue}' is not compatible with --xmlToYaml; the command must be '{DatabaseExecutorCommand.CodeGen}'.");
DatabaseConsole.WriteMasthead();
- return await CodeGenFileManager.ConvertXmlToYamlAsync(CommandType.Database, CodeGenFileManager.GetConfigFilename(CommandType.Database, Company, AppName)).ConfigureAwait(false);
+ return await CodeGenFileManager.ConvertXmlToYamlAsync(CommandType.Database, CodeGenFileManager.GetConfigFilename(_exeDir, CommandType.Database, Company, AppName)).ConfigureAwait(false);
}
var script = so.HasValue() ? so.Value() : _script;
@@ -222,7 +227,7 @@ public async Task RunAsync(string[] args)
private string ReplaceMoustache(string text, string command, string connectionString, string assembly, string script)
{
text = text.Replace("{{Command}}", command, StringComparison.OrdinalIgnoreCase);
- text = text.Replace("{{ConfigFile}}", CodeGen.CodeGenFileManager.GetConfigFilename(CodeGen.CommandType.Database, Company, AppName), StringComparison.OrdinalIgnoreCase);
+ text = text.Replace("{{ConfigFile}}", CodeGen.CodeGenFileManager.GetConfigFilename(_exeDir, CodeGen.CommandType.Database, Company, AppName), StringComparison.OrdinalIgnoreCase);
text = text.Replace("{{ConnectionString}}", connectionString, StringComparison.OrdinalIgnoreCase);
text = text.Replace("{{Assembly}}", assembly, StringComparison.OrdinalIgnoreCase);
text = text.Replace("{{Company}}", Company, StringComparison.OrdinalIgnoreCase);
diff --git a/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj b/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj
index 6f05f712b..5b1e08be7 100644
--- a/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj
+++ b/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj
@@ -2,7 +2,7 @@
netcoreapp3.1
- 4.1.7
+ 4.1.8Beef DevelopersAvanadefalse
diff --git a/tools/Beef.Test.NUnit/CHANGELOG.md b/tools/Beef.Test.NUnit/CHANGELOG.md
index 07934cef6..46bfc9e0e 100644
--- a/tools/Beef.Test.NUnit/CHANGELOG.md
+++ b/tools/Beef.Test.NUnit/CHANGELOG.md
@@ -2,6 +2,11 @@
Represents the **NuGet** versions.
+## v4.1.8
+- *Enhancement:* Added `EventData.Source` to the test output log.
+- *Enhancement:* Updated the event testing to support new `IEventDataContentSerializer` and `IEventDataConverter`.
+- *Enhancement:* Added `Response.Content` to the test detail summary where failure due to invalid status code.
+
## v4.1.7
- *Enhancement:* Updated all dependent NuGet packages to their latest respective version.
diff --git a/tools/Beef.Test.NUnit/EventSubscriberTester.cs b/tools/Beef.Test.NUnit/EventSubscriberTester.cs
index 05eadc983..793131956 100644
--- a/tools/Beef.Test.NUnit/EventSubscriberTester.cs
+++ b/tools/Beef.Test.NUnit/EventSubscriberTester.cs
@@ -106,8 +106,8 @@ public EventSubscriberTester ExpectUnhandledException(stri
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -314,7 +314,7 @@ public async Task RunAsync(EventData @event) where TSubscriber : IE
{
foreach (var e in events)
{
- TestContext.Out.WriteLine($" Subject: {e.Subject}, Action: {e.Action}");
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
}
}
@@ -327,7 +327,7 @@ public async Task RunAsync(EventData @event) where TSubscriber : IE
{
foreach (var e in events)
{
- TestContext.Out.WriteLine($" Subject: {e.Subject}, Action: {e.Action}");
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
}
}
diff --git a/tools/Beef.Test.NUnit/Events/ExpectEvent.cs b/tools/Beef.Test.NUnit/Events/ExpectEvent.cs
index 50dccefa6..95958fba1 100644
--- a/tools/Beef.Test.NUnit/Events/ExpectEvent.cs
+++ b/tools/Beef.Test.NUnit/Events/ExpectEvent.cs
@@ -70,8 +70,8 @@ public static void IsNotSent(string template, string? action = null, string? cor
}
///
- /// Verifies that the are sent (in order specified). The expected events can use wildcards for and
- /// optionally define . Use where comparisons are required (otherwise no comparison will occur).
+ /// Verifies that the are sent (in order specified). The expected events can use wildcards for and
+ /// optionally define . Use where comparisons are required (otherwise no comparison will occur).
/// Finally, the remaining properties are not compared.
///
/// The correlation identifier.
diff --git a/tools/Beef.Test.NUnit/Tests/AgentTest.cs b/tools/Beef.Test.NUnit/Tests/AgentTest.cs
index aca171638..0054c89e6 100644
--- a/tools/Beef.Test.NUnit/Tests/AgentTest.cs
+++ b/tools/Beef.Test.NUnit/Tests/AgentTest.cs
@@ -61,8 +61,8 @@ public AgentTest ExpectMessages(params string[] messages)
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -74,8 +74,8 @@ public AgentTest ExpectEvent(string template, string action)
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The will be compared against the . Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The will be compared against the . Finally, the remaining properties are not compared.
///
/// The event value .
/// The expected subject template (or fully qualified subject).
diff --git a/tools/Beef.Test.NUnit/Tests/AgentTestBase.cs b/tools/Beef.Test.NUnit/Tests/AgentTestBase.cs
index 05f9d429c..f8dbea758 100644
--- a/tools/Beef.Test.NUnit/Tests/AgentTestBase.cs
+++ b/tools/Beef.Test.NUnit/Tests/AgentTestBase.cs
@@ -141,6 +141,8 @@ protected void ResultCheck(WebApiAgentResult result, Stopwatch sw)
else
TestContext.Out.WriteLine($"{(string.IsNullOrEmpty(result.Content) ? "none" : result.Content)}");
+ var content = $"Content: {json ?? (string.IsNullOrEmpty(result.Content) ? "none" : result.Content)}";
+
TestContext.Out.WriteLine("");
TestContext.Out.WriteLine($"EVENTS PUBLISHED >");
var events = ExpectEvent.GetPublishedEvents(CorrelationId);
@@ -150,7 +152,7 @@ protected void ResultCheck(WebApiAgentResult result, Stopwatch sw)
{
foreach (var e in events)
{
- TestContext.Out.WriteLine($" Subject: {e.Subject}, Action: {e.Action}");
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
}
}
@@ -163,7 +165,7 @@ protected void ResultCheck(WebApiAgentResult result, Stopwatch sw)
{
foreach (var e in events)
{
- TestContext.Out.WriteLine($" Subject: {e.Subject}, Action: {e.Action}");
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
}
}
@@ -186,13 +188,13 @@ protected void ResultCheck(WebApiAgentResult result, Stopwatch sw)
// Perform checks.
if (_expectedStatusCode.HasValue && _expectedStatusCode != result.StatusCode)
- Assert.Fail($"Expected HttpStatusCode was '{_expectedStatusCode} ({(int)_expectedStatusCode})'; actual was {result.StatusCode} ({(int)result.StatusCode}).");
+ Assert.Fail($"Expected HttpStatusCode was '{_expectedStatusCode} ({(int)_expectedStatusCode})'; actual was {result.StatusCode} ({(int)result.StatusCode}).{Environment.NewLine}{Environment.NewLine}{content}");
if (_expectedErrorType.HasValue && _expectedErrorType != result.ErrorType)
- Assert.Fail($"Expected ErrorType was '{_expectedErrorType}'; actual was '{result.ErrorType}'.");
+ Assert.Fail($"Expected ErrorType was '{_expectedErrorType}'; actual was '{result.ErrorType}'.{Environment.NewLine}{Environment.NewLine}{content}");
if (_expectedErrorMessage != null && _expectedErrorMessage != result.ErrorMessage)
- Assert.Fail($"Expected ErrorMessage was '{_expectedErrorMessage}'; actual was '{result.ErrorMessage}'.");
+ Assert.Fail($"Expected ErrorMessage was '{_expectedErrorMessage}'; actual was '{result.ErrorMessage}'.{Environment.NewLine}{Environment.NewLine}{content}");
if (_expectedMessages != null)
TesterBase.CompareExpectedVsActualMessages(_expectedMessages, result.Messages);
diff --git a/tools/Beef.Test.NUnit/Tests/AgentTestCore.cs b/tools/Beef.Test.NUnit/Tests/AgentTestCore.cs
index ad550ceec..682b21fb5 100644
--- a/tools/Beef.Test.NUnit/Tests/AgentTestCore.cs
+++ b/tools/Beef.Test.NUnit/Tests/AgentTestCore.cs
@@ -120,8 +120,8 @@ protected void SetExpectMessages(MessageItemCollection messages)
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -131,8 +131,8 @@ protected void SetExpectEvent(string template, string action)
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The will be compared against the . Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The will be compared against the . Finally, the remaining properties are not compared.
///
/// The event value .
/// Indicates whether to use the returned value.
diff --git a/tools/Beef.Test.NUnit/Tests/AgentTestV.cs b/tools/Beef.Test.NUnit/Tests/AgentTestV.cs
index 125b109db..502667b39 100644
--- a/tools/Beef.Test.NUnit/Tests/AgentTestV.cs
+++ b/tools/Beef.Test.NUnit/Tests/AgentTestV.cs
@@ -200,8 +200,8 @@ public AgentTest ExpectUniqueKey()
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -213,8 +213,8 @@ public AgentTest ExpectEvent(string template, string a
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The will be compared against the . Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The will be compared against the . Finally, the remaining properties are not compared.
///
/// The event value .
/// The expected subject template (or fully qualified subject).
@@ -229,8 +229,8 @@ public AgentTest ExpectEvent(string template, strin
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The returned value () will be compared against the .
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The returned value () will be compared against the .
/// Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
diff --git a/tools/Beef.Test.NUnit/Tests/EventSubscriberTestHost.cs b/tools/Beef.Test.NUnit/Tests/EventSubscriberTestHost.cs
index bbc24ba56..ef13cb892 100644
--- a/tools/Beef.Test.NUnit/Tests/EventSubscriberTestHost.cs
+++ b/tools/Beef.Test.NUnit/Tests/EventSubscriberTestHost.cs
@@ -2,6 +2,7 @@
using Beef.Events;
using Microsoft.Extensions.Logging;
+using System;
using System.Diagnostics;
using System.Threading.Tasks;
@@ -29,6 +30,23 @@ public EventSubscriberTestHost UseLogger(ILogger logger)
return this;
}
+ ///
+ /// Gets the from the .
+ ///
+ /// The .
+ /// The .
+ protected override Task<(EventMetadata? Metadata, Exception? Exception)> GetMetadataAsync(IEventSubscriberData data)
+ {
+ try
+ {
+ return Task.FromResult<(EventMetadata?, Exception?)>(((EventMetadata?)data.Originating, null));
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult<(EventMetadata?, Exception?)>((null, ex));
+ }
+ }
+
///
/// Performs the receive processing for instance.
///
@@ -42,7 +60,7 @@ public async Task ReceiveAsync(EventData @event)
try
{
- Result = await ReceiveAsync(new EventDataSubscriberData(@event), (subscriber) => { WasSubscribed = true; return @event; }).ConfigureAwait(false);
+ Result = await ReceiveAsync(new EventDataSubscriberData(@event), (subscriber) => { WasSubscribed = true; return Task.FromResult(@event); }).ConfigureAwait(false);
}
catch (EventSubscriberUnhandledException essex)
{
diff --git a/tools/Beef.Test.NUnit/Tests/GrpcAgentTest.cs b/tools/Beef.Test.NUnit/Tests/GrpcAgentTest.cs
index 3043e80d4..bdeb0e4c3 100644
--- a/tools/Beef.Test.NUnit/Tests/GrpcAgentTest.cs
+++ b/tools/Beef.Test.NUnit/Tests/GrpcAgentTest.cs
@@ -61,8 +61,8 @@ public GrpcAgentTest ExpectMessages(params string[] messages)
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -74,8 +74,8 @@ public GrpcAgentTest ExpectEvent(string template, string actio
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The will be compared against the . Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The will be compared against the . Finally, the remaining properties are not compared.
///
/// The event value .
/// The expected subject template (or fully qualified subject).
diff --git a/tools/Beef.Test.NUnit/Tests/GrpcAgentTestBase.cs b/tools/Beef.Test.NUnit/Tests/GrpcAgentTestBase.cs
index 1dd0b14b6..07c777985 100644
--- a/tools/Beef.Test.NUnit/Tests/GrpcAgentTestBase.cs
+++ b/tools/Beef.Test.NUnit/Tests/GrpcAgentTestBase.cs
@@ -77,16 +77,29 @@ protected void ResultCheck(GrpcAgentResult result, Stopwatch sw)
if (result.Response != null)
TestContext.Out.WriteLine(JsonConvert.SerializeObject(result.Response, Formatting.Indented));
+ TestContext.Out.WriteLine("");
+ TestContext.Out.WriteLine($"EVENTS PUBLISHED >");
+ var events = ExpectEvent.GetPublishedEvents(CorrelationId);
+ if (events.Count == 0)
+ TestContext.Out.WriteLine(" None.");
+ else
+ {
+ foreach (var e in events)
+ {
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
+ }
+ }
+
TestContext.Out.WriteLine("");
TestContext.Out.WriteLine($"EVENTS SENT (Send invocation count: {Events.ExpectEvent.GetSendCount(CorrelationId)}) >");
- var events = ExpectEvent.GetSentEvents();
+ events = ExpectEvent.GetSentEvents(CorrelationId);
if (events.Count == 0)
TestContext.Out.WriteLine(" None.");
else
{
foreach (var e in events)
{
- TestContext.Out.WriteLine($" Subject: {e.Subject}, Action: {e.Action}");
+ TestContext.Out.WriteLine($" Subject: {e.Subject ?? ""}, Action: {e.Action ?? ""}, Source: {e.Source?.ToString() ?? ""}");
}
}
diff --git a/tools/Beef.Test.NUnit/Tests/GrpcAgentTestV.cs b/tools/Beef.Test.NUnit/Tests/GrpcAgentTestV.cs
index 3e81852a8..75aab298b 100644
--- a/tools/Beef.Test.NUnit/Tests/GrpcAgentTestV.cs
+++ b/tools/Beef.Test.NUnit/Tests/GrpcAgentTestV.cs
@@ -200,8 +200,8 @@ public GrpcAgentTest ExpectUniqueKey()
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . No value comparison will occur. Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . No value comparison will occur. Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).
/// The optional expected action; null indicates any.
@@ -213,8 +213,8 @@ public GrpcAgentTest ExpectEvent(string template, stri
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The will be compared against the . Finally, the remaining properties are not compared.
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The will be compared against the . Finally, the remaining properties are not compared.
///
/// The event value .
/// The expected subject template (or fully qualified subject).
@@ -229,8 +229,8 @@ public GrpcAgentTest ExpectEvent(string template, s
}
///
- /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
- /// . The returned value () will be compared against the .
+ /// Verifies that the the event is published (in order specified). The expected event can use wildcards for and optionally define
+ /// . The returned value () will be compared against the .
/// Finally, the remaining properties are not compared.
///
/// The expected subject template (or fully qualified subject).