diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f410124c9..78d02b596 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -25,6 +25,7 @@ jobs: Bar_ConnectionStrings__Database: Data Source=localhost,1433;Initial Catalog=Foo.Bar;User id=sa;Password=sAPWD23.^0;TrustServerCertificate=true Bam_ConnectionStrings__Database: Server=localhost;Port=3306;Database=Foo.Bam;Uid=ciuser;Pwd=ciStrong#!Password;AllowUserVariables=true;UseAffectedRows=false; Bap_ConnectionStrings__Database: Server=localhost;Port=5432;Database=Foo.Bap;User Id=postgres;Pwd=ciStrong#!Password; + Bac_CosmosConnectionString: ${{ secrets.COSMOS_CONNECTION_STRING }} Cdr_CosmosConnectionString: ${{ secrets.COSMOS_CONNECTION_STRING }} services: @@ -329,4 +330,17 @@ jobs: - name: Template/SqlServer/EfWs services test working-directory: ./Foo.EfWs.Bar/Foo.EfWs.Bar.Services.Test + run: dotnet test + + # Template - CosmosDB + + - name: Template/Cosmos create + run: dotnet new beef --company Foo.Co --appname Bac --datasource Cosmos --output Foo.Co.Bac + + - name: Template/Cosmos code-gen + working-directory: ./Foo.Co.Bac/Foo.Co.Bac.CodeGen + run: dotnet run all + + - name: Template/Cosmos test + working-directory: ./Foo.Co.Bac run: dotnet test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f546303b..bc2623a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Represents the **NuGet** versions. +## v5.15.0 +- *Enhancement:* Added `Operation.Query` boolean to enable support for OData-like query syntax. This leverages the underlying `CoreEx.Data.Querying` (`v3.25.1+`) capabilities to enable. The `Operation.Behavior` has also been extended to support a '`Q`'uery as a shorthand to enable a query-based operation. _Note:_ this is an **awesome** new capability. +- *Enhancement:* Updated the `DatabaseName`, `EntityFrameworkName`, `CosmosName`, `ODataName` and `HttpAgentName` to support both `Type` (existing) and optional `Name` (new). This uses the `Type^Name` syntax supported for other properties with similar purpose. The properties have also had the `Name` suffix renamed to `Type` as this more accurately reflects the property intent (existing names will continue to work with a corresponding warning during code-generation). + ## v5.14.2 - *Fixed:* Fixed the data model code-generation to output the `PartitionKey` where specified. - *Fixed:* Fixed the code-generated `PartitionKey` to be a nullable string. diff --git a/Common.targets b/Common.targets index 9b345f33a..f2778ae06 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@ - 5.14.2 + 5.15.0 preview Avanade Avanade diff --git a/docs/Entity-CodeGeneration-Config.md b/docs/Entity-CodeGeneration-Config.md index 8cfdcd56b..0a591004a 100644 --- a/docs/Entity-CodeGeneration-Config.md +++ b/docs/Entity-CodeGeneration-Config.md @@ -135,7 +135,7 @@ Provides the _Database Data-layer_ configuration. Property | Description -|- -**`databaseName`** | The .NET database interface name (used where `Operation.AutoImplement` is `Database`).
† Defaults to `IDatabase`. This can be overridden within the `Entity`(s). +**`databaseType`** | The .NET database type and optional name (used where `Operation.AutoImplement` is `Database`).
† Defaults to `IDatabase`. Should be formatted as `Type` + `^` + `Name`; e.g. `IDatabase^Db`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s). **`databaseSchema`** | The default database schema name.
† Defaults to `dbo`. **`databaseProvider`** | The default database schema name. Valid options are: `SqlServer`, `MySQL`, `Postgres`.
† Defaults to `SqlServer`. Enables specific database provider functionality/formatting/etc. where applicable. `databaseMapperEx` | Indicates that a `DatabaseMapperEx` will be used; versus, `DatabaseMapper` (which uses Reflection internally).
† Defaults to `true`. The `DatabaseMapperEx` essentially replaces the `DatabaseMapper` as it is more performant (extended/explicit); this option can be used where leagcy/existing behavior is required. @@ -147,7 +147,7 @@ Provides the _Entity Framewotrk (EF) Data-layer_ configuration. Property | Description -|- -`entityFrameworkName` | The .NET Entity Framework interface name used where `Operation.AutoImplement` is `EntityFramework`.
† Defaults to `IEfDb`. This can be overridden within the `Entity`(s). +`entityFrameworkType` | The .NET Entity Framework type and optional name (used where `Operation.AutoImplement` is `EntityFramework`).
† Defaults to `IEfDb`. Should be formatted as `Type` + `^` + `Name`; e.g. `IEfDb^Ef`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).
@@ -156,7 +156,7 @@ Provides the _CosmosDB Data-layer_ configuration. Property | Description -|- -**`cosmosName`** | The .NET Entity Framework interface name used where `Operation.AutoImplement` is `Cosmos`.
† Defaults to `ICosmosDb`. This can be overridden within the `Entity`(s). +**`cosmosType`** | The .NET Cosmos DB type and name (used where `Operation.AutoImplement` is `Cosmos`).
† Defaults to `ICosmosDb`. Should be formatted as `Type` + `^` + `Name`; e.g. `ICosmosDb^Cosmos`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).
@@ -165,7 +165,7 @@ Provides the _OData Data-layer_ configuration. Property | Description -|- -**`odataName`** | The .NET OData interface name used where `Operation.AutoImplement` is `OData`.
† Defaults to `IOData`. This can be overridden within the `Entity`(s). +**`odataType`** | The .NET OData interface name used where `Operation.AutoImplement` is `OData`.
† Defaults to `IOData`. Should be formatted as `Type` + `^` + `Name`; e.g. `IOData^OData`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).
@@ -174,7 +174,7 @@ Provides the _HTTP Agent Data-layer_ configuration. Property | Description -|- -**`httpAgentName`** | The default .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`.
† Defaults to `IHttpAgent`. This can be overridden within the `Entity`(s). +**`httpAgentType`** | The default .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`.
† Defaults to `IHttpAgent`. Should be formatted as `Type` + `^` + `Name`; e.g. `IHttpAgent^HttpAgent`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).
diff --git a/docs/Entity-Entity-Config.md b/docs/Entity-Entity-Config.md index ebf54929f..96e0f0d2f 100644 --- a/docs/Entity-Entity-Config.md +++ b/docs/Entity-Entity-Config.md @@ -121,10 +121,11 @@ Provides the _Operation_ configuration. These primarily provide a shorthand to c Property | Description -|- -`behavior` | Defines the key CRUD-style behavior (operation types), being 'C'reate, 'G'et (or 'R'ead), 'U'pdate, 'P'atch and 'D'elete). Additionally, GetByArgs ('B') and GetAll ('A') operations that will be automatically generated where not otherwise explicitly specified.
† Value may only specifiy one or more of the `CGRUDBA` characters (in any order) to define the automatically generated behavior (operations); for example: `CRUPD` or `CRUP` or `rba` (case insensitive). This is shorthand for setting one or more of the following properties: `Get`, `GetByArgs`, `GetAll`, 'Create', `Update`, `Patch` and `Delete`. Where one of these properties is set to either `true` or `false` this will take precedence over the value set for `Behavior`. +`behavior` | Defines the key CRUD-style behavior (operation types), being 'C'reate, 'G'et (or 'R'ead), 'U'pdate, 'P'atch, 'D'elete and `Q`uery). Additionally, `GetByArgs` ('B'), `GetAll` ('A') and `GetByQuery` ('Q') operations configuration will be automatically inferred where not otherwise explicitly specified.
† Value may only specifiy one or more of the `CGRUDBAQ` characters (in any order) to define the automatically generated behavior (operations); for example: `CRUPD` or `CRUP` or `rba` (case insensitive). This is shorthand for setting one or more of the following properties: `Get`, `GetByArgs`, `GetAll`, 'Create', `Update`, `Patch` and `Delete`. Where one of these properties is set to either `true` or `false` this will take precedence over the value set for `Behavior`. `get` | Indicates that a `Get` operation will be automatically generated where not otherwise explicitly specified. `getByArgs` | Indicates that a `GetByArgs` operation will be automatically generated where not otherwise explicitly specified. `getAll` | Indicates that a `GetAll` operation will be automatically generated where not otherwise explicitly specified. +`getByQuery` | Indicates that a `GetByQuery` operation will be automatically generated where not otherwise explicitly specified. `create` | Indicates that a `Create` operation will be automatically generated where not otherwise explicitly specified. `update` | Indicates that a `Update` operation will be automatically generated where not otherwise explicitly specified. `patch` | Indicates that a `Patch` operation will be automatically generated where not otherwise explicitly specified. @@ -217,7 +218,7 @@ Provides the specific _Database (ADO.NET)_ configuration where `AutoImplement` i Property | Description -|- -**`databaseName`** | The .NET database interface name (used where `AutoImplement` is `Database`).
† Defaults to the `CodeGeneration.DatabaseName` configuration property (its default value is `IDatabase`). +**`databaseType`** | The .NET database type and optional name (used where `AutoImplement` is `Database`).
† Defaults to the `CodeGeneration.DatabaseName` configuration property (its default value is `IDatabase`). Should be formatted as `Type` + `^` + `Name`. **`databaseSchema`** | The database schema name (used where `AutoImplement` is `Database`).
† Defaults to `dbo`. `databaseMapperInheritsFrom` | The name of the `Mapper` that the generated Database `Mapper` inherits from. `databaseCustomMapper` | Indicates that a custom Database `Mapper` will be used; i.e. not generated.
† Otherwise, by default, a `Mapper` will be generated. @@ -230,7 +231,7 @@ Provides the specific _Entity Framework (EF)_ configuration where `AutoImplement Property | Description -|- -**`entityFrameworkName`** | The .NET Entity Framework interface name used where `AutoImplement` is `EntityFramework`.
† Defaults to `CodeGeneration.EntityFrameworkName`. +**`entityFrameworkType`** | The .NET Entity Framework type and optyional name used where `AutoImplement` is `EntityFramework`.
† Defaults to `CodeGeneration.EntityFrameworkName`. Should be formatted as `Type` + `^` + `Name`. **`entityFrameworkModel`** | The corresponding Entity Framework model name (required where `AutoImplement` is `EntityFramework`). `entityFrameworkCustomMapper` | Indicates that a custom Entity Framework `Mapper` will be used; i.e. not generated.
† Otherwise, by default, a `Mapper` will be generated. `entityFrameworkMapperBase` | The EntityFramework data-layer name that should be used for base mappings. @@ -242,7 +243,7 @@ Provides the specific _Cosmos_ configuration where `AutoImplement` is `Cosmos`. Property | Description -|- -**`cosmosName`** | The .NET Cosmos interface name used where `AutoImplement` is `Cosmos`.
† Defaults to the `CodeGeneration.CosmosName` configuration property (its default value is `ICosmosDb`). +**`cosmosType`** | The .NET Cosmos DB type and optional name used where `AutoImplement` is `Cosmos`.
† Defaults to the `CodeGeneration.CosmosName` configuration property (its default value is `ICosmosDb`). Should be formatted as `Type` + `^` + `Name`. **`cosmosModel`** | The corresponding Cosmos model name (required where `AutoImplement` is `Cosmos`). **`cosmosContainerId`** | The Cosmos `ContainerId` required where `AutoImplement` is `Cosmos`. `cosmosPartitionKey` | The C# code to be used for setting the optional Cosmos `PartitionKey` where `AutoImplement` is `Cosmos`.
† The value `PartitionKey.None` can be specified. Literals will need to be quoted. @@ -258,7 +259,7 @@ Provides the specific _OData_ configuration where `AutoImplement` is `OData`. Property | Description -|- -**`odataName`** | The .NET OData interface name used where `AutoImplement` is `OData`.
† Defaults to the `CodeGeneration.ODataName` configuration property (its default value is `IOData`). +**`odataType`** | The .NET OData type and optional name used where `AutoImplement` is `OData`.
† Defaults to the `CodeGeneration.ODataName` configuration property (its default value is `IOData`). Should be formatted as `Type` + `^` + `Name`. **`odataModel`** | The corresponding OData model name (required where `AutoImplement` is `OData`). **`odataCollectionName`** | The name of the underlying OData collection where `AutoImplement` is `OData`.
† The underlying `Simple.OData.Client` will attempt to infer. `odataCustomMapper` | Indicates that a custom OData `Mapper` will be used; i.e. not generated.
† Otherwise, by default, a `Mapper` will be generated. @@ -271,7 +272,7 @@ Provides the specific _HTTP Agent_ configuration where `AutoImplement` is `HttpA Property | Description -|- -**`httpAgentName`** | The .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`.
† Defaults to `CodeGeneration.HttpAgentName` configuration property (its default value is `IHttpAgent`). +**`httpAgentType`** | The .NET HTTP Agent type and optional name used where `Operation.AutoImplement` is `HttpAgent`.
† Defaults to `CodeGeneration.HttpAgentName` configuration property (its default value is `IHttpAgent`). Should be formatted as `Type` + `^` + `Name`. `httpAgentRoutePrefix` | The base HTTP Agent API route where `Operation.AutoImplement` is `HttpAgent`.
† This is the base (prefix) `URI` for the HTTP Agent endpoint and can be further extended when defining the underlying `Operation`(s). **`httpAgentModel`** | The corresponding HTTP Agent model name (required where `AutoImplement` is `HttpAgent`).
† This can be overridden within the `Operation`(s). `httpAgentReturnModel` | The corresponding HTTP Agent model name (required where `AutoImplement` is `HttpAgent`).
† This can be overridden within the `Operation`(s). diff --git a/docs/Entity-Operation-Config.md b/docs/Entity-Operation-Config.md index 1ebfe70ea..4587b6f0b 100644 --- a/docs/Entity-Operation-Config.md +++ b/docs/Entity-Operation-Config.md @@ -69,6 +69,7 @@ Property | Description `text` | The text for use in comments.
† The `Text` will be defaulted for all the `Operation.Type` options with the exception of `Custom`. To create a `` within use moustache shorthand (e.g. {{Xxx}}). To have the text used as-is prefix with a `+` plus-sign character. **`primaryKey`** | Indicates whether the properties marked as a primary key (`Property.PrimaryKey`) are to be used as the parameters.
† This simplifies the specification of these properties as parameters versus having to declare each specifically. Each of the parameters will also be set to be mandatory. **`paging`** | Indicates whether a `PagingArgs` argument is to be added to the operation to enable (standardized) paging related logic. +**`query`** | Indicates whether a `QueryArgs` argument is to be added to the operation to enable OData-like $filter and $orderby related logic. `valueType` | The .NET value parameter `Type` for the operation.
† Defaults to the parent `Entity.Name` where the `Operation.Type` options are `Create` or `Update`. `returnType` | The .NET return `Type` for the operation.
† Defaults to the parent `Entity.Name` where the `Operation.Type` options are `Get`, `GetColl`, `Create` or `Update`; otherwise, defaults to `void`. `returnTypeNullable` | Indicates whether the `ReturnType` is nullable for the operation.
† Will be inferred where the `ReturnType` is denoted as nullable; i.e. suffixed by a `?`. Additionally a `Type` of `Get` will default to `true` where not specified. diff --git a/samples/Cdr.Banking/Cdr.Banking.Api/Cdr.Banking.Api.csproj b/samples/Cdr.Banking/Cdr.Banking.Api/Cdr.Banking.Api.csproj index 612ebf2b4..ceca19e82 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Api/Cdr.Banking.Api.csproj +++ b/samples/Cdr.Banking/Cdr.Banking.Api/Cdr.Banking.Api.csproj @@ -5,8 +5,8 @@ true
- - + + diff --git a/samples/Cdr.Banking/Cdr.Banking.Api/Controllers/Generated/AccountController.cs b/samples/Cdr.Banking/Cdr.Banking.Api/Controllers/Generated/AccountController.cs index 14abc37c4..5bf66d117 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Api/Controllers/Generated/AccountController.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Api/Controllers/Generated/AccountController.cs @@ -43,6 +43,19 @@ public Task GetAccounts([FromQuery(Name="product-category")] stri return _webApi.GetWithResultAsync(Request, p => _manager.GetAccountsAsync(args, p.RequestOptions.Paging), alternateStatusCode: HttpStatusCode.NoContent); } + /// + /// Get all accounts. + /// + /// The Account array + [Tags("Banking", "Accounts")] + [HttpGet("api/v1/banking/accounts/query", Name="Account_GetAccountsQuery")] + [Paging] + [Query] + [ProducesResponseType(typeof(Common.Entities.AccountCollection), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task GetAccountsQuery() + => _webApi.GetWithResultAsync(Request, p => _manager.GetAccountsQueryAsync(p.RequestOptions.Query, p.RequestOptions.Paging), alternateStatusCode: HttpStatusCode.NoContent); + /// /// Get AccountDetail. /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Api/Startup.cs b/samples/Cdr.Banking/Cdr.Banking.Api/Startup.cs index 0011f4a60..560c88980 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Api/Startup.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Api/Startup.cs @@ -30,11 +30,17 @@ public void ConfigureServices(IServiceCollection services) .AddValidators(); // Add the cosmos database. - services.AddSingleton(sp => + services.AddSingleton(sp => { var settings = sp.GetRequiredService(); var cco = new AzCosmos.CosmosClientOptions { SerializerOptions = new AzCosmos.CosmosSerializationOptions { PropertyNamingPolicy = AzCosmos.CosmosPropertyNamingPolicy.CamelCase, IgnoreNullValues = true } }; - return new CosmosDb(new AzCosmos.CosmosClient(settings.CosmosConnectionString, cco).GetDatabase(settings.CosmosDatabaseId), sp.GetRequiredService()); + return new AzCosmos.CosmosClient(settings.CosmosConnectionString, cco); + }); + + services.AddCosmosDb(sp => + { + var settings = sp.GetRequiredService(); + return new CosmosDb(sp.GetRequiredService().GetDatabase(settings.CosmosDatabaseId), sp.GetRequiredService()); }); // Add the generated reference data services. @@ -60,6 +66,7 @@ public void ConfigureServices(IServiceCollection services) options.SwaggerDoc("v1", new OpenApiInfo { Title = "Cdr.Banking API", Version = "v1" }); options.OperationFilter(); // Needed to support AcceptsBodyAttribute where body parameter not explicitly defined. options.OperationFilter(); // Needed to support PagingAttribute where PagingArgs parameter not explicitly defined. + options.OperationFilter(); // Needed to support QueryAttribute where QueryArgs parameter not explicitly defined. }); } diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Cdr.Banking.Business.csproj b/samples/Cdr.Banking/Cdr.Banking.Business/Cdr.Banking.Business.csproj index 98ab4fac9..95f927756 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Cdr.Banking.Business.csproj +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Cdr.Banking.Business.csproj @@ -12,8 +12,8 @@ - - - + + +
\ No newline at end of file diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/AccountData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/AccountData.cs index dcb8fc7b1..652af717d 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/AccountData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/AccountData.cs @@ -5,12 +5,19 @@ namespace Cdr.Banking.Business.Data; public partial class AccountData { + private static QueryArgsConfig _config = QueryArgsConfig.Create() + .WithFilter(filter => filter + .AddReferenceDataField(nameof(Model.Account.OpenStatus), c => c.WithValue(os => os == OpenStatus.All ? throw new FormatException("Value not valid for filtering.") : os)) + .AddReferenceDataField(nameof(Model.Account.ProductCategory)) + .AddField(nameof(Model.Account.IsOwned))); + /// /// Initializes a new instance of the class setting the required internal configurations. /// partial void AccountDataCtor() { _getAccountsOnQuery = GetAccountsOnQuery; // Wire up the plug-in to enable filtering. + _getAccountsQueryOnQuery = (q, args) => q.Where(_config, args).OrderBy(x => x.Id); // Wire up the OData-like query syntax. } /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/CosmosDb.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/CosmosDb.cs index d7656c8f7..eb898c288 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/CosmosDb.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/CosmosDb.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.Metrics; - -namespace Cdr.Banking.Business.Data; +namespace Cdr.Banking.Business.Data; /// /// Represents the CosmosDb/DocumentDb client. diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountData.cs index 90082d3aa..953149950 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountData.cs @@ -11,6 +11,7 @@ public partial class AccountData : IAccountData { private readonly ICosmos _cosmos; private Func, AccountArgs?, IQueryable>? _getAccountsOnQuery; + private Func, QueryArgs?, IQueryable>? _getAccountsQueryOnQuery; /// /// Initializes a new instance of the class. @@ -25,6 +26,10 @@ public AccountData(ICosmos cosmos) public Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging) => _cosmos.Accounts.Query(q => _getAccountsOnQuery?.Invoke(q, args) ?? q).WithPaging(paging).SelectResultWithResultAsync(); + /// + public Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging) + => _cosmos.Accounts.Query(q => _getAccountsQueryOnQuery?.Invoke(q, query) ?? q).WithPaging(paging).SelectResultWithResultAsync(); + /// public Task> GetDetailAsync(string? accountId) => _cosmos.AccountDetails.GetWithResultAsync(accountId); @@ -36,7 +41,7 @@ public Task> GetAccountsAsync(AccountArgs? args, public Task> GetStatementAsync(string? accountId) => GetStatementOnImplementationAsync(accountId); /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -61,7 +66,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountDetailData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountDetailData.cs index 6018fa3a1..16bb5af62 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountDetailData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/AccountDetailData.cs @@ -11,7 +11,7 @@ public partial class AccountDetailData { /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -34,7 +34,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalanceData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalanceData.cs index dfea11957..aa1bb7894 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalanceData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalanceData.cs @@ -11,7 +11,7 @@ public partial class BalanceData { /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -33,7 +33,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalancePurseData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalancePurseData.cs index adf27d225..a731dff5a 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalancePurseData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/BalancePurseData.cs @@ -11,7 +11,7 @@ public partial class BalancePurseData { /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -29,7 +29,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/CreditCardAccountData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/CreditCardAccountData.cs index 6114a4c30..f0991b833 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/CreditCardAccountData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/CreditCardAccountData.cs @@ -11,7 +11,7 @@ public partial class CreditCardAccountData { /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -31,7 +31,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/IAccountData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/IAccountData.cs index ce3659667..1935163d1 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/IAccountData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/IAccountData.cs @@ -17,6 +17,14 @@ public partial interface IAccountData /// The . Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging); + /// + /// Get all accounts. + /// + /// The . + /// The . + /// The . + Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Get . /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TermDepositAccountData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TermDepositAccountData.cs index 76b606ed3..0508d5687 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TermDepositAccountData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TermDepositAccountData.cs @@ -11,7 +11,7 @@ public partial class TermDepositAccountData { /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -32,7 +32,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TransactionData.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TransactionData.cs index d8d7805a3..67cfe4334 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TransactionData.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Data/Generated/TransactionData.cs @@ -26,7 +26,7 @@ public Task> GetTransactionsAsync(string? ac => _cosmos.Transactions.Query(new Mac.PartitionKey(accountId), q => _getTransactionsOnQuery?.Invoke(q, accountId, args) ?? q).WithPaging(paging).SelectResultWithResultAsync(); /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -58,7 +58,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { 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 e0849326e..af42405d4 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/AccountDataSvc.cs @@ -25,6 +25,9 @@ public AccountDataSvc(IAccountData data, IRequestCache cache) /// public Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging) => _data.GetAccountsAsync(args, paging); + /// + public Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging) => _data.GetAccountsQueryAsync(query, paging); + /// public Task> GetDetailAsync(string? accountId) => Result.Go().CacheGetOrAddAsync(_cache, accountId, () => _data.GetDetailAsync(accountId)); diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/IAccountDataSvc.cs b/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/IAccountDataSvc.cs index ccd38dd53..2518c8909 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/IAccountDataSvc.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/DataSvc/Generated/IAccountDataSvc.cs @@ -17,6 +17,14 @@ public partial interface IAccountDataSvc /// The . Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging); + /// + /// Get all accounts. + /// + /// The . + /// The . + /// The . + Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Get . /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Generated/AccountManager.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Generated/AccountManager.cs index 332d6b26f..0342183de 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Generated/AccountManager.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Generated/AccountManager.cs @@ -28,6 +28,13 @@ public Task> GetAccountsAsync(AccountArgs? args, .ThenAsAsync(() => _dataService.GetAccountsAsync(args, paging)); }, InvokerArgs.Read); + /// + public Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging) => ManagerInvoker.Current.InvokeAsync(this, (_, ct) => + { + return Result.Go() + .ThenAsAsync(() => _dataService.GetAccountsQueryAsync(query, paging)); + }, InvokerArgs.Read); + /// public Task> GetDetailAsync(string? accountId) => ManagerInvoker.Current.InvokeAsync(this, (_, ct) => { diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/Generated/IAccountManager.cs b/samples/Cdr.Banking/Cdr.Banking.Business/Generated/IAccountManager.cs index 432e4a8aa..64d138fe3 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/Generated/IAccountManager.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/Generated/IAccountManager.cs @@ -17,6 +17,14 @@ public partial interface IAccountManager /// The . Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging); + /// + /// Get all accounts. + /// + /// The . + /// The . + /// The . + Task> GetAccountsQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Get . /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Business/GlobalUsings.cs b/samples/Cdr.Banking/Cdr.Banking.Business/GlobalUsings.cs index a1ea938b1..18eb75503 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Business/GlobalUsings.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Business/GlobalUsings.cs @@ -2,6 +2,7 @@ global using CoreEx.Caching; global using CoreEx.Configuration; global using CoreEx.Cosmos; +global using CoreEx.Data.Querying; global using CoreEx.Entities; global using CoreEx.Entities.Extended; global using CoreEx.Events; diff --git a/samples/Cdr.Banking/Cdr.Banking.CodeGen/entity.beef-5.yaml b/samples/Cdr.Banking/Cdr.Banking.CodeGen/entity.beef-5.yaml index 6d3bc1c33..bdb56ba2a 100644 --- a/samples/Cdr.Banking/Cdr.Banking.CodeGen/entity.beef-5.yaml +++ b/samples/Cdr.Banking/Cdr.Banking.CodeGen/entity.beef-5.yaml @@ -1,4 +1,4 @@ -cosmosName: ICosmos +cosmosType: ICosmos entities: # Account as per the defined schema, including corresponding collection/result. # API route prefixed defined. @@ -37,6 +37,10 @@ entities: { name: Args, type: AccountArgs, validator: AccountArgsValidator } ] }, + # Operation to get all Accounts for the user with OData-like $filter. + # Supports paging. + # Data access will be auto-implemented for Cosmos as defined for the entity. + { name: GetAccountsQuery, text: Get all accounts, type: GetColl, query: true, webApiRoute: query, paging: true, webApiTags: [ ^, Accounts ] }, # Operation to get the AccountDetail for a specified account. # Operation attached to Account for logical grouping. # Returns AccountDetail (the DataEntityMapper is overridden to ensure correct mapper is used). diff --git a/samples/Cdr.Banking/Cdr.Banking.CodeGen/refdata.beef-5.yaml b/samples/Cdr.Banking/Cdr.Banking.CodeGen/refdata.beef-5.yaml index c01af604f..b2a432490 100644 --- a/samples/Cdr.Banking/Cdr.Banking.CodeGen/refdata.beef-5.yaml +++ b/samples/Cdr.Banking/Cdr.Banking.CodeGen/refdata.beef-5.yaml @@ -1,4 +1,4 @@ -cosmosName: ICosmos +cosmosType: ICosmos webApiRoutePrefix: api/v1/ref refDataType: Guid autoImplement: Cosmos diff --git a/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/AccountAgent.cs b/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/AccountAgent.cs index 505b2e29f..395209525 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/AccountAgent.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/AccountAgent.cs @@ -32,6 +32,10 @@ public AccountAgent(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.Ex public Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) => GetAsync("api/v1/banking/accounts", requestOptions: requestOptions.IncludePaging(paging), args: HttpArgs.Create(new HttpArg("args", args, HttpArgType.FromUriUseProperties)), cancellationToken: cancellationToken); + /// + public Task> GetAccountsQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + => GetAsync("api/v1/banking/accounts/query", requestOptions: requestOptions.IncludeQuery(query).IncludePaging(paging), args: HttpArgs.Create(), cancellationToken: cancellationToken); + /// public Task> GetDetailAsync(string? accountId, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) => GetAsync("api/v1/banking/accounts/{accountId}", requestOptions: requestOptions, args: HttpArgs.Create(new HttpArg("accountId", accountId)), cancellationToken: cancellationToken); diff --git a/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/IAccountAgent.cs b/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/IAccountAgent.cs index 51de7378e..6842fd6bc 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/IAccountAgent.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Common/Agents/Generated/IAccountAgent.cs @@ -32,6 +32,16 @@ public partial interface IAccountAgent /// A . Task> GetAccountsAsync(AccountArgs? args, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + /// + /// Get all accounts. + /// + /// The . + /// The . + /// The optional . + /// The . + /// A . + Task> GetAccountsQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + /// /// Get . /// diff --git a/samples/Cdr.Banking/Cdr.Banking.Common/Cdr.Banking.Common.csproj b/samples/Cdr.Banking/Cdr.Banking.Common/Cdr.Banking.Common.csproj index 659a46155..baa017c57 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Common/Cdr.Banking.Common.csproj +++ b/samples/Cdr.Banking/Cdr.Banking.Common/Cdr.Banking.Common.csproj @@ -8,6 +8,6 @@ - + \ No newline at end of file diff --git a/samples/Cdr.Banking/Cdr.Banking.Test/AccountTest.cs b/samples/Cdr.Banking/Cdr.Banking.Test/AccountTest.cs index a2abae5e4..f5fd2610d 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Test/AccountTest.cs +++ b/samples/Cdr.Banking/Cdr.Banking.Test/AccountTest.cs @@ -27,7 +27,7 @@ public void OneTimeSetUp() [Test] public void B110_GetAccounts_User1() { - var v = this.Agent() + var v = Agent() .ExpectStatusCode(HttpStatusCode.OK) .Run(a => a.GetAccountsAsync(null)).Value; @@ -98,7 +98,6 @@ public void B210_GetAccounts_OpenStatus() Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "12345678", "34567890" })); } - [Test] public void B220_GetAccounts_ProductCategory() { @@ -196,6 +195,99 @@ public void B330_GetAccounts_Page3() #endregion + #region GetAccountsQuery + + [Test] + public void B510_GetAccountsQuery_User1() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetAccountsQueryAsync(null)).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null); + Assert.That(v.Items, Has.Count.EqualTo(3)); + }); + Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "12345678", "34567890", "45678901" })); + } + + [Test] + public void B520_GetAccountsQuery_OpenStatus() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetAccountsQueryAsync(QueryArgs.Create("openstatus eq 'open'"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null); + Assert.That(v.Items, Has.Count.EqualTo(2)); + }); + Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "12345678", "34567890" })); + } + + [Test] + public void B521_GetAccountsQuery_OpenStatus_All() + { + Agent() + .ExpectStatusCode(HttpStatusCode.BadRequest) + .ExpectError("Field 'openstatus' with value 'all' is invalid: Value not valid for filtering.") + .Run(a => a.GetAccountsQueryAsync(QueryArgs.Create("openstatus eq 'all'"))); + } + + [Test] + public void B230_GetAccountsQuery_ProductCategory() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetAccountsQueryAsync(QueryArgs.Create("productcategory eq 'CRED_AND_CHRG_CARDS'"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null); + Assert.That(v.Items, Has.Count.EqualTo(1)); + }); + Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "34567890" })); + } + + [Test] + public void B240_GetAccountsQuery_IsOwned() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetAccountsQueryAsync(QueryArgs.Create("isowned eq true"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null); + Assert.That(v.Items, Has.Count.EqualTo(2)); + }); + Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "12345678", "34567890" })); + } + + [Test] + public void B250_GetAccountsQuery_NotIsOwned() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetAccountsQueryAsync(QueryArgs.Create("isowned eq false"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null); + Assert.That(v.Items, Has.Count.EqualTo(1)); + }); + Assert.That(v.Items.Select(x => x.Id).ToArray(), Is.EqualTo(new string[] { "45678901" })); + } + + #endregion + #region GetDetail [Test] diff --git a/samples/Cdr.Banking/Cdr.Banking.Test/Cdr.Banking.Test.csproj b/samples/Cdr.Banking/Cdr.Banking.Test/Cdr.Banking.Test.csproj index 9aabf4daf..df94f14de 100644 --- a/samples/Cdr.Banking/Cdr.Banking.Test/Cdr.Banking.Test.csproj +++ b/samples/Cdr.Banking/Cdr.Banking.Test/Cdr.Banking.Test.csproj @@ -34,12 +34,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/samples/Demo/Beef.Demo.Api/Beef.Demo.Api.csproj b/samples/Demo/Beef.Demo.Api/Beef.Demo.Api.csproj index f05bcea67..8e0fac016 100644 --- a/samples/Demo/Beef.Demo.Api/Beef.Demo.Api.csproj +++ b/samples/Demo/Beef.Demo.Api/Beef.Demo.Api.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/samples/Demo/Beef.Demo.Api/Controllers/Generated/ContactController.cs b/samples/Demo/Beef.Demo.Api/Controllers/Generated/ContactController.cs index a10eac53f..a93ee46b1 100644 --- a/samples/Demo/Beef.Demo.Api/Controllers/Generated/ContactController.cs +++ b/samples/Demo/Beef.Demo.Api/Controllers/Generated/ContactController.cs @@ -30,6 +30,18 @@ public ContactController(WebApi webApi, IContactManager manager, Microsoft.Exten partial void ContactControllerCtor(); // Enables additional functionality to be added to the constructor. + /// + /// Gets the Contact array that contains the items that match the selection criteria. + /// + /// The Contact array + [HttpGet("api/v1/contacts/query", Name="Contact_GetByQuery")] + [Paging] + [Query] + [ProducesResponseType(typeof(Common.Entities.ContactCollection), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task GetByQuery() + => _webApi.GetAsync(Request, p => _manager.GetByQueryAsync(p.RequestOptions.Query, p.RequestOptions.Paging), alternateStatusCode: HttpStatusCode.NoContent); + /// /// Gets the Contact array that contains the items that match the selection criteria. /// diff --git a/samples/Demo/Beef.Demo.Business/Beef.Demo.Business.csproj b/samples/Demo/Beef.Demo.Business/Beef.Demo.Business.csproj index a7188ddaa..1712b7dcd 100644 --- a/samples/Demo/Beef.Demo.Business/Beef.Demo.Business.csproj +++ b/samples/Demo/Beef.Demo.Business/Beef.Demo.Business.csproj @@ -16,14 +16,14 @@ - - - - - - - - + + + + + + + + diff --git a/samples/Demo/Beef.Demo.Business/Data/ContactData.cs b/samples/Demo/Beef.Demo.Business/Data/ContactData.cs index 41567a4c1..27fc74e94 100644 --- a/samples/Demo/Beef.Demo.Business/Data/ContactData.cs +++ b/samples/Demo/Beef.Demo.Business/Data/ContactData.cs @@ -1,15 +1,26 @@ -namespace Beef.Demo.Business.Data +using CoreEx.Data.Querying; + +namespace Beef.Demo.Business.Data { public partial class ContactData { + private readonly static QueryArgsConfig _config = QueryArgsConfig.Create() + .WithFilter(filter => filter + .AddField(nameof(Contact.LastName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddField(nameof(Contact.FirstName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddReferenceDataField(nameof(Contact.Status), nameof(EfModel.Contact.StatusCode))) + .WithOrderBy(orderBy => orderBy + .AddField(nameof(Contact.LastName)) + .AddField(nameof(Contact.FirstName)) + .WithDefault($"{nameof(Contact.LastName)}, {nameof(Contact.FirstName)}")); + + partial void ContactDataCtor() + { + _getByQueryOnQuery = (q, a) => q.Where(_config, a).OrderBy(_config, a); + } + private Task RaiseEventOnImplementationAsync(bool throwError) { - // Nesting invoker should result in the event being enqueued into the database (not committed though). - //await _ef.EventOutboxInvoker.InvokeAsync(this, () => - //{ - // _evtPub.Publish("Contact", "Made"); - // return Task.CompletedTask; - //}); _events.PublishEvent("Contact", "Made"); // The exception should result in a rollback of the database event enqueue - fingers crossed ;-) diff --git a/samples/Demo/Beef.Demo.Business/Data/Generated/ContactData.cs b/samples/Demo/Beef.Demo.Business/Data/Generated/ContactData.cs index b1b04bd4c..e2f214cc8 100644 --- a/samples/Demo/Beef.Demo.Business/Data/Generated/ContactData.cs +++ b/samples/Demo/Beef.Demo.Business/Data/Generated/ContactData.cs @@ -12,32 +12,37 @@ namespace Beef.Demo.Business.Data; /// public partial class ContactData : IContactData { - private readonly IEfDb _ef; + private readonly IEfDb _sqlEf; private readonly IEventPublisher _events; + private Func, QueryArgs?, IQueryable>? _getByQueryOnQuery; private Func, IQueryable>? _getAllOnQuery; /// /// Initializes a new instance of the class. /// - /// The . + /// The . /// The . - public ContactData(IEfDb ef, IEventPublisher events) - { _ef = ef.ThrowIfNull(); _events = events.ThrowIfNull(); ContactDataCtor(); } + public ContactData(IEfDb sqlEf, IEventPublisher events) + { _sqlEf = sqlEf.ThrowIfNull(); _events = events.ThrowIfNull(); ContactDataCtor(); } partial void ContactDataCtor(); // Enables additional functionality to be added to the constructor. + /// + public Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging) + => _sqlEf.Query(q => _getByQueryOnQuery?.Invoke(q, query) ?? q).WithPaging(paging).SelectResultAsync(); + /// public Task GetAllAsync() - => _ef.Query(q => _getAllOnQuery?.Invoke(q) ?? q).SelectResultAsync(); + => _sqlEf.Query(q => _getAllOnQuery?.Invoke(q) ?? q).SelectResultAsync(); /// public Task GetAsync(Guid id) - => _ef.GetAsync(id); + => _sqlEf.GetAsync(id); /// public Task CreateAsync(Contact value) => DataInvoker.Current.InvokeAsync(this, async (_, __) => { - var r = await _ef.CreateAsync(value).ConfigureAwait(false); + var r = await _sqlEf.CreateAsync(value).ConfigureAwait(false); _events.PublishValueEvent(r, new Uri($"/contact/{r.Id}", UriKind.Relative), $"Demo.Contact", "Create"); return r; }, new InvokerArgs { EventPublisher = _events }); @@ -45,7 +50,7 @@ public Task CreateAsync(Contact value) => DataInvoker.Current.InvokeAsy /// public Task UpdateAsync(Contact value) => DataInvoker.Current.InvokeAsync(this, async (_, __) => { - var r = await _ef.UpdateAsync(value).ConfigureAwait(false); + var r = await _sqlEf.UpdateAsync(value).ConfigureAwait(false); _events.PublishValueEvent(r, new Uri($"/contact/{r.Id}", UriKind.Relative), $"Demo.Contact", "Update"); return r; }, new InvokerArgs { EventPublisher = _events }); @@ -53,7 +58,7 @@ public Task UpdateAsync(Contact value) => DataInvoker.Current.InvokeAsy /// public Task DeleteAsync(Guid id) => DataInvoker.Current.InvokeAsync(this, async (_, __) => { - await _ef.DeleteAsync(id).ConfigureAwait(false); + await _sqlEf.DeleteAsync(id).ConfigureAwait(false); _events.PublishValueEvent(new Contact { Id = id }, new Uri($"/contact/{id}", UriKind.Relative), $"Demo.Contact", "Delete"); }, new InvokerArgs { EventPublisher = _events }); diff --git a/samples/Demo/Beef.Demo.Business/Data/Generated/IContactData.cs b/samples/Demo/Beef.Demo.Business/Data/Generated/IContactData.cs index fca0f3cf4..7b26229a7 100644 --- a/samples/Demo/Beef.Demo.Business/Data/Generated/IContactData.cs +++ b/samples/Demo/Beef.Demo.Business/Data/Generated/IContactData.cs @@ -12,6 +12,14 @@ namespace Beef.Demo.Business.Data; /// public partial interface IContactData { + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Gets the that contains the items that match the selection criteria. /// diff --git a/samples/Demo/Beef.Demo.Business/Data/Generated/RobotData.cs b/samples/Demo/Beef.Demo.Business/Data/Generated/RobotData.cs index da803a0ff..779f27a53 100644 --- a/samples/Demo/Beef.Demo.Business/Data/Generated/RobotData.cs +++ b/samples/Demo/Beef.Demo.Business/Data/Generated/RobotData.cs @@ -45,7 +45,7 @@ public Task> GetByArgsAsync(RobotArgs? args, Pagin => _cosmos.Items.Query(q => _getByArgsOnQuery?.Invoke(q, args) ?? q).WithPaging(paging).SelectResultWithResultAsync(); /// - /// Provides the to Entity Framework mapping. + /// Provides the to Cosmos mapping. /// public partial class EntityToModelCosmosMapper : Mapper { @@ -68,7 +68,7 @@ public EntityToModelCosmosMapper() } /// - /// Provides the Entity Framework to mapping. + /// Provides the Cosmos to mapping. /// public partial class ModelToEntityCosmosMapper : Mapper { diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs index 4b49c3428..38e497dd3 100644 --- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs +++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/ContactDataSvc.cs @@ -25,6 +25,9 @@ public ContactDataSvc(IContactData data, IRequestCache cache) partial void ContactDataSvcCtor(); // Enables additional functionality to be added to the constructor. + /// + public Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging) => _data.GetByQueryAsync(query, paging); + /// public Task GetAllAsync() => _data.GetAllAsync(); diff --git a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/IContactDataSvc.cs b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/IContactDataSvc.cs index 92c5cddf1..5d8aa5c37 100644 --- a/samples/Demo/Beef.Demo.Business/DataSvc/Generated/IContactDataSvc.cs +++ b/samples/Demo/Beef.Demo.Business/DataSvc/Generated/IContactDataSvc.cs @@ -12,6 +12,14 @@ namespace Beef.Demo.Business.DataSvc; /// public partial interface IContactDataSvc { + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Gets the that contains the items that match the selection criteria. /// diff --git a/samples/Demo/Beef.Demo.Business/Entities/Generated/EyeColor.cs b/samples/Demo/Beef.Demo.Business/Entities/Generated/EyeColor.cs index 299efcf49..e6b585e92 100644 --- a/samples/Demo/Beef.Demo.Business/Entities/Generated/EyeColor.cs +++ b/samples/Demo/Beef.Demo.Business/Entities/Generated/EyeColor.cs @@ -36,13 +36,13 @@ public partial class EyeColorCollection : ReferenceDataCollectionBase /// Initializes a new instance of the class. /// - public EyeColorCollection() { } + public EyeColorCollection() : base(ReferenceDataSortOrder.Code) { } /// /// Initializes a new instance of the class with to add. /// /// The items to add. - public EyeColorCollection(IEnumerable items) => AddRange(items); + public EyeColorCollection(IEnumerable items) : this() => AddRange(items); } #pragma warning restore diff --git a/samples/Demo/Beef.Demo.Business/Generated/ContactManager.cs b/samples/Demo/Beef.Demo.Business/Generated/ContactManager.cs index 037f62b44..44c1dc5a2 100644 --- a/samples/Demo/Beef.Demo.Business/Generated/ContactManager.cs +++ b/samples/Demo/Beef.Demo.Business/Generated/ContactManager.cs @@ -23,6 +23,12 @@ public ContactManager(IContactDataSvc dataService) partial void ContactManagerCtor(); // Enables additional functionality to be added to the constructor. + /// + public Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging) => ManagerInvoker.Current.InvokeAsync(this, async (_, ct) => + { + return await _dataService.GetByQueryAsync(query, paging).ConfigureAwait(false); + }, InvokerArgs.Read); + /// public Task GetAllAsync() => ManagerInvoker.Current.InvokeAsync(this, async (_, ct) => { diff --git a/samples/Demo/Beef.Demo.Business/Generated/IContactManager.cs b/samples/Demo/Beef.Demo.Business/Generated/IContactManager.cs index 2c1364de8..2524fc777 100644 --- a/samples/Demo/Beef.Demo.Business/Generated/IContactManager.cs +++ b/samples/Demo/Beef.Demo.Business/Generated/IContactManager.cs @@ -12,6 +12,14 @@ namespace Beef.Demo.Business; /// public partial interface IContactManager { + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Gets the that contains the items that match the selection criteria. /// diff --git a/samples/Demo/Beef.Demo.CodeGen/contact.entity.beef-5.yaml b/samples/Demo/Beef.Demo.CodeGen/contact.entity.beef-5.yaml index 4ecb3490a..07d3126ad 100644 --- a/samples/Demo/Beef.Demo.CodeGen/contact.entity.beef-5.yaml +++ b/samples/Demo/Beef.Demo.CodeGen/contact.entity.beef-5.yaml @@ -1,6 +1,6 @@ entities: # Entity with no etag or changelog -- { name: Contact, collection: true, collectionResult: true, isInitialOverride: false, validator: ContactValidator, webApiRoutePrefix: api/v1/contacts, webApiAuthorize: AllowAnonymous, webApiCtorParams: [ Microsoft.Extensions.Configuration.IConfiguration^Config ], eventPublish: Data, eventOutbox: Database, get: true, create: true, update: true, patch: true, delete: true, getAll: true, managerCleanUp: false, autoImplement: EntityFramework, databaseSchema: Demo, entityFrameworkModel: EfModel.Contact, +- { name: Contact, collection: true, collectionResult: true, isInitialOverride: false, validator: ContactValidator, webApiRoutePrefix: api/v1/contacts, webApiAuthorize: AllowAnonymous, webApiCtorParams: [ Microsoft.Extensions.Configuration.IConfiguration^Config ], eventPublish: Data, eventOutbox: Database, get: true, create: true, update: true, patch: true, delete: true, getAll: true, getByQuery: true, managerCleanUp: false, autoImplement: EntityFramework, databaseSchema: Demo, entityFrameworkModel: EfModel.Contact, entityFrameworkName: IEfDb^SqlEf, properties: [ { name: Id, text: '{{Contact}} identifier', type: Guid, primaryKey: true, dataAutoGenerated: true, dataName: ContactId }, { name: FirstName, type: string }, diff --git a/samples/Demo/Beef.Demo.CodeGen/refdata.beef-5.yaml b/samples/Demo/Beef.Demo.CodeGen/refdata.beef-5.yaml index e33ff1526..51b4cde33 100644 --- a/samples/Demo/Beef.Demo.CodeGen/refdata.beef-5.yaml +++ b/samples/Demo/Beef.Demo.CodeGen/refdata.beef-5.yaml @@ -25,7 +25,7 @@ entities: ] } -- { name: EyeColor, refDataType: Guid, collection: true, webApiRoutePrefix: api/v1/demo/ref/eyeColors, autoImplement: EntityFramework, databaseSchema: Ref, entityFrameworkModel: EfModel.EyeColor } +- { name: EyeColor, refDataType: Guid, refDataSortOrder: Code, collection: true, webApiRoutePrefix: api/v1/demo/ref/eyeColors, autoImplement: EntityFramework, databaseSchema: Ref, entityFrameworkModel: EfModel.EyeColor } - { name: PowerSource, refDataType: Guid, collection: true, webApiRoutePrefix: api/v1/demo/ref/powerSources, autoImplement: Cosmos, cosmosContainerId: RefData, cosmosValueContainer: true, cosmosModel: Model.PowerSource, dataModel: true, properties: [ diff --git a/samples/Demo/Beef.Demo.Common/Agents/Generated/ContactAgent.cs b/samples/Demo/Beef.Demo.Common/Agents/Generated/ContactAgent.cs index 1d03a29ea..93902ad07 100644 --- a/samples/Demo/Beef.Demo.Common/Agents/Generated/ContactAgent.cs +++ b/samples/Demo/Beef.Demo.Common/Agents/Generated/ContactAgent.cs @@ -31,6 +31,10 @@ public partial class ContactAgent : TypedHttpClientBase, IContactA /// The . public ContactAgent(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext) : base(client, jsonSerializer, executionContext) { } + /// + public Task> GetByQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + => GetAsync("api/v1/contacts/query", requestOptions: requestOptions.IncludeQuery(query).IncludePaging(paging), args: HttpArgs.Create(), cancellationToken: cancellationToken); + /// public Task> GetAllAsync(HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) => GetAsync("api/v1/contacts", requestOptions: requestOptions, cancellationToken: cancellationToken); diff --git a/samples/Demo/Beef.Demo.Common/Agents/Generated/IContactAgent.cs b/samples/Demo/Beef.Demo.Common/Agents/Generated/IContactAgent.cs index 2c817b76e..43554e83b 100644 --- a/samples/Demo/Beef.Demo.Common/Agents/Generated/IContactAgent.cs +++ b/samples/Demo/Beef.Demo.Common/Agents/Generated/IContactAgent.cs @@ -25,6 +25,16 @@ namespace Beef.Demo.Common.Agents /// public partial interface IContactAgent { + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The optional . + /// The . + /// A . + Task> GetByQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + /// /// Gets the that contains the items that match the selection criteria. /// diff --git a/samples/Demo/Beef.Demo.Common/Beef.Demo.Common.csproj b/samples/Demo/Beef.Demo.Common/Beef.Demo.Common.csproj index 85a6fb35b..16608b57a 100644 --- a/samples/Demo/Beef.Demo.Common/Beef.Demo.Common.csproj +++ b/samples/Demo/Beef.Demo.Common/Beef.Demo.Common.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/Demo/Beef.Demo.Test/Beef.Demo.Test.csproj b/samples/Demo/Beef.Demo.Test/Beef.Demo.Test.csproj index 8b4c4e48b..1e65cf812 100644 --- a/samples/Demo/Beef.Demo.Test/Beef.Demo.Test.csproj +++ b/samples/Demo/Beef.Demo.Test/Beef.Demo.Test.csproj @@ -52,8 +52,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -61,7 +61,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/Demo/Beef.Demo.Test/ContactTest.cs b/samples/Demo/Beef.Demo.Test/ContactTest.cs index 0f1c2b6f7..1802c6441 100644 --- a/samples/Demo/Beef.Demo.Test/ContactTest.cs +++ b/samples/Demo/Beef.Demo.Test/ContactTest.cs @@ -2,6 +2,7 @@ using Beef.Demo.Common.Agents; using Beef.Demo.Common.Entities; using CoreEx.Database; +using CoreEx.Entities; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using NUnit.Framework.Internal; @@ -203,6 +204,45 @@ public void A150_GetAll() Assert.That(r2.Response.Headers?.ETag?.Tag, Is.Not.EqualTo(etag)); } + [Test] + public void A155a_GetByQuery() + { + using var test = ApiTester.Create(); + + var r = test.Agent().With() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 'c')"))).Value; + + Assert.That(r.Items, Has.Count.EqualTo(1)); + + r = test.Agent().With() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 'm')"))).Value; + + Assert.That(r.Items, Has.Count.EqualTo(0)); + } + + [Test] + public void A155b_GetByQuery() + { + using var test = ApiTester.Create(); + + for (int i = 0; i < 10; i++) + { + var r = test.Agent().With() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 'c')"))).Value; + + Assert.That(r.Items, Has.Count.EqualTo(1)); + + r = test.Agent().With() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 'm')"))).Value; + + Assert.That(r.Items, Has.Count.EqualTo(0)); + } + } + [Test] public void A160_Delete() { diff --git a/samples/My.Hr/My.Hr.Api/My.Hr.Api.csproj b/samples/My.Hr/My.Hr.Api/My.Hr.Api.csproj index c464d87a7..0930e53f1 100644 --- a/samples/My.Hr/My.Hr.Api/My.Hr.Api.csproj +++ b/samples/My.Hr/My.Hr.Api/My.Hr.Api.csproj @@ -5,9 +5,9 @@ true - - - + + + diff --git a/samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj b/samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj index 75d85f4f4..371ce3d9c 100644 --- a/samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj +++ b/samples/My.Hr/My.Hr.Business/My.Hr.Business.csproj @@ -6,10 +6,10 @@ latest - - - - + + + + \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Common/My.Hr.Common.csproj b/samples/My.Hr/My.Hr.Common/My.Hr.Common.csproj index a19023fa7..f4b34c8d1 100644 --- a/samples/My.Hr/My.Hr.Common/My.Hr.Common.csproj +++ b/samples/My.Hr/My.Hr.Common/My.Hr.Common.csproj @@ -4,6 +4,6 @@ enable - + \ No newline at end of file diff --git a/samples/My.Hr/My.Hr.Test/Apis/EmployeeTest.cs b/samples/My.Hr/My.Hr.Test/Apis/EmployeeTest.cs index f95e97fbb..c119cf423 100644 --- a/samples/My.Hr/My.Hr.Test/Apis/EmployeeTest.cs +++ b/samples/My.Hr/My.Hr.Test/Apis/EmployeeTest.cs @@ -243,7 +243,7 @@ public void A300_GetByArgs_ArgsError() Agent() .ExpectStatusCode(HttpStatusCode.BadRequest) .ExpectErrors("Genders contains one or more invalid items.") - .Run(a => a.GetByArgsAsync(new EmployeeArgs { Genders = new List { "Q" } })); + .Run(a => a.GetByArgsAsync(new EmployeeArgs { Genders = new List { "Z" } })); } #endregion diff --git a/samples/My.Hr/My.Hr.Test/My.Hr.Test.csproj b/samples/My.Hr/My.Hr.Test/My.Hr.Test.csproj index 13cc99901..2ce09738b 100644 --- a/samples/My.Hr/My.Hr.Test/My.Hr.Test.csproj +++ b/samples/My.Hr/My.Hr.Test/My.Hr.Test.csproj @@ -32,17 +32,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/samples/MyEf.Hr/MyEf.Hr.Api/Controllers/Generated/EmployeeController.cs b/samples/MyEf.Hr/MyEf.Hr.Api/Controllers/Generated/EmployeeController.cs index 721e627ce..ca9828abf 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Api/Controllers/Generated/EmployeeController.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Api/Controllers/Generated/EmployeeController.cs @@ -98,6 +98,18 @@ public Task GetByArgs(string? firstName = default, string? lastNa return _webApi.GetWithResultAsync(Request, p => _manager.GetByArgsAsync(args, p.RequestOptions.Paging), alternateStatusCode: HttpStatusCode.NoContent); } + /// + /// Gets the EmployeeBase array that contains the items that match the selection criteria. + /// + /// The EmployeeBase array + [HttpGet("employees/query", Name="Employee_GetByQuery")] + [Paging] + [Query] + [ProducesResponseType(typeof(Common.Entities.EmployeeBaseCollection), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NoContent)] + public Task GetByQuery() + => _webApi.GetWithResultAsync(Request, p => _manager.GetByQueryAsync(p.RequestOptions.Query, p.RequestOptions.Paging), alternateStatusCode: HttpStatusCode.NoContent); + /// /// Terminates an existing Employee. /// diff --git a/samples/MyEf.Hr/MyEf.Hr.Api/MyEf.Hr.Api.csproj b/samples/MyEf.Hr/MyEf.Hr.Api/MyEf.Hr.Api.csproj index 96b4ae2ff..45f9b1259 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Api/MyEf.Hr.Api.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Api/MyEf.Hr.Api.csproj @@ -6,12 +6,12 @@ True - - + + - + diff --git a/samples/MyEf.Hr/MyEf.Hr.Api/Startup.cs b/samples/MyEf.Hr/MyEf.Hr.Api/Startup.cs index 1aa37d740..5ad8b862c 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Api/Startup.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Api/Startup.cs @@ -103,6 +103,7 @@ public void ConfigureServices(IServiceCollection services) options.OperationFilter(); // Needed to support AcceptsBodyAttribute where body parameter not explicitly defined. options.OperationFilter(); // Needed to support PagingAttribute where PagingArgs parameter not explicitly defined. + options.OperationFilter(); // Needed to support QueryAttribute where QueryArgs parameter not explicitly defined. }); } diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/Data/EmployeeData.cs b/samples/MyEf.Hr/MyEf.Hr.Business/Data/EmployeeData.cs index 2127fb1c6..45ca126b9 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/Data/EmployeeData.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/Data/EmployeeData.cs @@ -2,6 +2,18 @@ public partial class EmployeeData { + private static readonly QueryArgsConfig _config = QueryArgsConfig.Create() + .WithFilter(filter => filter + .AddField(nameof(Employee.LastName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddField(nameof(Employee.FirstName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddReferenceDataField(nameof(Employee.Gender), nameof(EfModel.Employee.GenderCode)) + .AddField(nameof(Employee.StartDate)) + .AddNullField(nameof(Employee.Termination), nameof(EfModel.Employee.TerminationDate), c => c.WithDefault(new QueryStatement($"{nameof(EfModel.Employee.TerminationDate)} == null")))) + .WithOrderBy(orderby => orderby + .AddField(nameof(Employee.LastName)) + .AddField(nameof(Employee.FirstName)) + .WithDefault($"{nameof(Employee.LastName)}, {nameof(Employee.FirstName)}")); + partial void EmployeeDataCtor() { // Implement the GetByArgs OnQuery search/filtering logic. @@ -18,6 +30,9 @@ partial void EmployeeDataCtor() return q.IgnoreAutoIncludes().OrderBy(x => x.LastName).ThenBy(x => x.FirstName).ThenBy(x => x.StartDate); }; + + // Implement the GetByQuery OnQuery search/filtering logic using OData-like query syntax. + _getByQueryOnQuery = (q, args) => q.IgnoreAutoIncludes().Where(_config, args).OrderBy(_config, args); } /// @@ -30,5 +45,5 @@ private Task> TerminateOnImplementationAsync(TerminationDetail .When(e => e.Termination is not null, _ => Result.ValidationError("An Employee can not be terminated more than once.")) .When(e => value.Date < e.StartDate, _ => Result.ValidationError("An Employee can not be terminated prior to their start date.")) .Then(e => e.Termination = value) - .ThenAsAsync(e => UpdateAsync(e)); + .ThenAsAsync(UpdateAsync); } \ No newline at end of file diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/EmployeeData.cs b/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/EmployeeData.cs index 96ab14bc0..c13062b88 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/EmployeeData.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/EmployeeData.cs @@ -11,6 +11,7 @@ public partial class EmployeeData : IEmployeeData { private readonly IEfDb _ef; private Func, EmployeeArgs?, IQueryable>? _getByArgsOnQuery; + private Func, QueryArgs?, IQueryable>? _getByQueryOnQuery; /// /// Initializes a new instance of the class. @@ -41,6 +42,10 @@ public Task DeleteAsync(Guid id) public Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging) => _ef.Query(q => _getByArgsOnQuery?.Invoke(q, args) ?? q).WithPaging(paging).SelectResultWithResultAsync(); + /// + public Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging) + => _ef.Query(q => _getByQueryOnQuery?.Invoke(q, query) ?? q).WithPaging(paging).SelectResultWithResultAsync(); + /// public Task> TerminateAsync(TerminationDetail value, Guid id) => TerminateOnImplementationAsync(value, id); diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/IEmployeeData.cs b/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/IEmployeeData.cs index 6320c6d9e..14f78534a 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/IEmployeeData.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/Data/Generated/IEmployeeData.cs @@ -44,6 +44,14 @@ public partial interface IEmployeeData /// The . Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging); + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Terminates an existing . /// diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs b/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs index 5125978b1..c7a390fb7 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/EmployeeDataSvc.cs @@ -54,6 +54,9 @@ public Task DeleteAsync(Guid id) => DataSvcInvoker.Current.InvokeAsync(t /// public Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging) => _data.GetByArgsAsync(args, paging); + /// + public Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging) => _data.GetByQueryAsync(query, paging); + /// public Task> TerminateAsync(TerminationDetail value, Guid id) => DataSvcInvoker.Current.InvokeAsync(this, (_, __) => { diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/IEmployeeDataSvc.cs b/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/IEmployeeDataSvc.cs index 627086fea..84dbfbac4 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/IEmployeeDataSvc.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/DataSvc/Generated/IEmployeeDataSvc.cs @@ -44,6 +44,14 @@ public partial interface IEmployeeDataSvc /// The . Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging); + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Terminates an existing . /// diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/Generated/EmployeeManager.cs b/samples/MyEf.Hr/MyEf.Hr.Business/Generated/EmployeeManager.cs index bdf697ea4..2d764bb2e 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/Generated/EmployeeManager.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/Generated/EmployeeManager.cs @@ -59,6 +59,13 @@ public Task> GetByArgsAsync(EmployeeArgs? a .ThenAsAsync(() => _dataService.GetByArgsAsync(args, paging)); }, InvokerArgs.Read); + /// + public Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging) => ManagerInvoker.Current.InvokeAsync(this, (_, ct) => + { + return Result.Go() + .ThenAsAsync(() => _dataService.GetByQueryAsync(query, paging)); + }, InvokerArgs.Read); + /// public Task> TerminateAsync(TerminationDetail value, Guid id) => ManagerInvoker.Current.InvokeAsync(this, (_, ct) => { diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/Generated/IEmployeeManager.cs b/samples/MyEf.Hr/MyEf.Hr.Business/Generated/IEmployeeManager.cs index ac690523c..7fbc4ca0f 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/Generated/IEmployeeManager.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/Generated/IEmployeeManager.cs @@ -45,6 +45,14 @@ public partial interface IEmployeeManager /// The . Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging); + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The . + Task> GetByQueryAsync(QueryArgs? query, PagingArgs? paging); + /// /// Terminates an existing . /// diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/GlobalUsings.cs b/samples/MyEf.Hr/MyEf.Hr.Business/GlobalUsings.cs index 2a4681bff..9d478143a 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/GlobalUsings.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Business/GlobalUsings.cs @@ -1,6 +1,7 @@ global using CoreEx; global using CoreEx.Caching; global using CoreEx.Configuration; +global using CoreEx.Data.Querying; global using CoreEx.Database; global using CoreEx.Database.SqlServer; global using CoreEx.Database.SqlServer.Outbox; diff --git a/samples/MyEf.Hr/MyEf.Hr.Business/MyEf.Hr.Business.csproj b/samples/MyEf.Hr/MyEf.Hr.Business/MyEf.Hr.Business.csproj index 579a3a90d..c0c4934de 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Business/MyEf.Hr.Business.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Business/MyEf.Hr.Business.csproj @@ -6,11 +6,11 @@ latest - - - - - + + + + + diff --git a/samples/MyEf.Hr/MyEf.Hr.CodeGen/entity.beef-5.yaml b/samples/MyEf.Hr/MyEf.Hr.CodeGen/entity.beef-5.yaml index 96c3b91e3..47a803b07 100644 --- a/samples/MyEf.Hr/MyEf.Hr.CodeGen/entity.beef-5.yaml +++ b/samples/MyEf.Hr/MyEf.Hr.CodeGen/entity.beef-5.yaml @@ -78,6 +78,13 @@ entities: { name: Args, type: EmployeeArgs, validator: EmployeeArgsValidator } ] }, + # Query operation + # - Type is GetColl that indicates that a collection is the expected result for the query-based operation. + # - ReturnType is overriding the default Employee as we want to use EmployeeBase (reduced set of fields). + # - Paging indicates that paging support is required and to be automatically enabled for the operation. + # - Query indicates that OData-like $filter/$order support is to be enabled. + # - WebApiRoute specifies the route of 'query' to be used for the operation. + { name: GetByQuery, type: GetColl, query: true, paging: true, returnType: EmployeeBase, webApiRoute: query }, # Terminate operation # - Text is specified to override the default for an Update. # - Type is Update as it follows a similar operation pattern. diff --git a/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/EmployeeAgent.cs b/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/EmployeeAgent.cs index facf942dd..19107c43a 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/EmployeeAgent.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/EmployeeAgent.cs @@ -52,6 +52,10 @@ public Task DeleteAsync(Guid id, HttpRequestOptions? requestOptions public Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) => GetAsync("employees", requestOptions: requestOptions.IncludePaging(paging), args: HttpArgs.Create(new HttpArg("args", args, HttpArgType.FromUriUseProperties)), cancellationToken: cancellationToken); + /// + public Task> GetByQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + => GetAsync("employees/query", requestOptions: requestOptions.IncludeQuery(query).IncludePaging(paging), args: HttpArgs.Create(), cancellationToken: cancellationToken); + /// public Task> TerminateAsync(TerminationDetail value, Guid id, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) => PostAsync("employees/{id}/terminate", value, requestOptions: requestOptions, args: HttpArgs.Create(new HttpArg("id", id)), cancellationToken: cancellationToken); diff --git a/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/IEmployeeAgent.cs b/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/IEmployeeAgent.cs index 528eda70a..bf4cd314e 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/IEmployeeAgent.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Common/Agents/Generated/IEmployeeAgent.cs @@ -80,6 +80,16 @@ public partial interface IEmployeeAgent /// A . Task> GetByArgsAsync(EmployeeArgs? args, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + /// + /// Gets the that contains the items that match the selection criteria. + /// + /// The . + /// The . + /// The optional . + /// The . + /// A . + Task> GetByQueryAsync(QueryArgs? query = null, PagingArgs? paging = null, HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + /// /// Terminates an existing . /// diff --git a/samples/MyEf.Hr/MyEf.Hr.Common/MyEf.Hr.Common.csproj b/samples/MyEf.Hr/MyEf.Hr.Common/MyEf.Hr.Common.csproj index 003b2d775..b94aa3d1c 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Common/MyEf.Hr.Common.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Common/MyEf.Hr.Common.csproj @@ -5,6 +5,6 @@ True - + \ No newline at end of file diff --git a/samples/MyEf.Hr/MyEf.Hr.Security.Subscriptions/MyEf.Hr.Security.Subscriptions.csproj b/samples/MyEf.Hr/MyEf.Hr.Security.Subscriptions/MyEf.Hr.Security.Subscriptions.csproj index 896719cd7..9b997003a 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Security.Subscriptions/MyEf.Hr.Security.Subscriptions.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Security.Subscriptions/MyEf.Hr.Security.Subscriptions.csproj @@ -15,14 +15,14 @@ - - - - + + + + - + - + diff --git a/samples/MyEf.Hr/MyEf.Hr.Security.Test/MyEf.Hr.Security.Test.csproj b/samples/MyEf.Hr/MyEf.Hr.Security.Test/MyEf.Hr.Security.Test.csproj index 523d52f5c..d83ccf7cf 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Security.Test/MyEf.Hr.Security.Test.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Security.Test/MyEf.Hr.Security.Test.csproj @@ -16,10 +16,10 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,7 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/MyEf.Hr/MyEf.Hr.Test/Apis/EmployeeTest.cs b/samples/MyEf.Hr/MyEf.Hr.Test/Apis/EmployeeTest.cs index 5d034e4fa..8fa09c056 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Test/Apis/EmployeeTest.cs +++ b/samples/MyEf.Hr/MyEf.Hr.Test/Apis/EmployeeTest.cs @@ -236,7 +236,150 @@ public void A300_GetByArgs_ArgsError() Agent() .ExpectStatusCode(HttpStatusCode.BadRequest) .ExpectErrors("Genders contains one or more invalid items.") - .Run(a => a.GetByArgsAsync(new EmployeeArgs { Genders = ["Q"] })); + .Run(a => a.GetByArgsAsync(new EmployeeArgs { Genders = ["Z"] })); + } + + #endregion + + #region GetByQuery + + [Test] + public void A410_GetByQuery_All() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync()).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(3)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Jones", "Smithers" })); + }); + } + + [Test] + public void A420_GetByQuery_All_Paging() + { + var r = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("termination eq null or termination ne null"), PagingArgs.CreateSkipAndTake(1, 2))); + + var v = r.Value; + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Jones", "Smith" })); + }); + + // Query again with etag and ensure not modified. + Agent() + .ExpectStatusCode(HttpStatusCode.NotModified) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("termination eq null or termination ne null"), PagingArgs.CreateSkipAndTake(1, 2), new HttpRequestOptions { ETag = r.Response!.Headers!.ETag!.Tag })); + } + + [Test] + public void A430_GetByQuery_FirstName() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("contains(firstname, 'a')"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Smithers" })); + }); + } + + [Test] + public void A440_GetByQuery_LastName() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 's')"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(1)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Smithers" })); + }); + } + + [Test] + public void A450_GetByQuery_LastName_IncludeTerminated() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 's') and (termination eq null or termination ne null)"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Smith", "Smithers" })); + }); + } + + [Test] + public void A460_GetByQuery_Gender() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender in ('f')"))).Value; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Jones" })); + }); + } + + [Test] + public void A470_GetByQuery_Empty() + { + Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 's') and startswith(firstname, 'b') and gender eq 'f'"))) + .AssertJson("[]"); + } + + [Test] + public void A480_GetByQuery_FieldSelection() + { + var r = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'f'").Include("firstname", "lastname"))) + .AssertJson("[{\"firstName\":\"Rachael\",\"lastName\":\"Browne\"},{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}]"); + } + + [Test] + public void A490_GetByQuery_RefDataText() + { + var r = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'f'"), requestOptions: new HttpRequestOptions { IncludeText = true })); + + Assert.That(r.Value, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(r.Value.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(r.Value.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Jones" })); + Assert.That(r.Value.Items.Select(x => x.GenderText).ToArray(), Is.EqualTo(new string[] { "Female", "Female" })); + }); + } + + [Test] + public void A500_GetByQuery_ArgsError() + { + Agent() + .ExpectStatusCode(HttpStatusCode.BadRequest) + .ExpectErrors("Field 'gender' with value 'z' is invalid: Not a valid Gender.") + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'z'"))); } #endregion diff --git a/samples/MyEf.Hr/MyEf.Hr.Test/MyEf.Hr.Test.csproj b/samples/MyEf.Hr/MyEf.Hr.Test/MyEf.Hr.Test.csproj index caae12688..ec8c57a4f 100644 --- a/samples/MyEf.Hr/MyEf.Hr.Test/MyEf.Hr.Test.csproj +++ b/samples/MyEf.Hr/MyEf.Hr.Test/MyEf.Hr.Test.csproj @@ -32,17 +32,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/samples/MyEf.Hr/docs/5-Employee-Search.md b/samples/MyEf.Hr/docs/5-Employee-Search.md index a7490f26e..10c84e95e 100644 --- a/samples/MyEf.Hr/docs/5-Employee-Search.md +++ b/samples/MyEf.Hr/docs/5-Employee-Search.md @@ -7,7 +7,7 @@ This will walk through the process of creating and testing the employee search c ## Functional requirement The employee search will allow the following criteria to be searched: -- First and last name using wildcard; e.g. `Smi*`. +- First and last name using wildcard (starts with, ends with and contains); e.g. `abc*`, `*abc` or `*abc*`. - Gender selection; zero, one or more. - Start date range (from/to). - Option to include terminated employees (default is to exclude). @@ -147,7 +147,7 @@ public class EmployeeArgsValidator : Validator ## End-to-End testing -Now that we've implemented GetByArgs search functionality, we can re-add the appropriate tests. Do so by un-commenting the region `GetByArgs` within `MyEf.Hr.Test/Apis/EmployeeTest.cs`. +Now that we've implemented `GetByArgs` search functionality, we can re-add the appropriate tests. Do so by un-commenting the region `GetByArgs` within `MyEf.Hr.Test/Apis/EmployeeTest.cs`. As extra homework, you should also consider implementing unit testing for the validator. @@ -165,7 +165,7 @@ Check the output of code gen tool. There should have been 2 new and 9 updated fi MyEf.Hr.CodeGen Complete. [1818ms, Files: Unchanged = 16, Updated = 9, Created = 2, TotalLines = 1584] ``` -Within test explorer, run the EmployeeTest set of tests and confirm they all pass. +Within test explorer, run the `EmployeeTest` set of tests and confirm they all pass. The following tests were newly added and should pass: @@ -184,6 +184,83 @@ A300_GetByArgs_ArgsError
+## OData-like Query + +The above search capability is great and for most use cases is perfectly acceptable; however, it is not as flexible as the likes of OData or GraphQL. + +Therefore, depending on the functional needs a more flexible query capability may be required. There is basic, explicit, OData-like support available where this need arises; this will now be added to the `MyEf.Hr` solution. See [`CoreEx.Data.Querying`](https://github.com/Avanade/CoreEx/tree/main/src/CoreEx.Data#odata-like-querying) for more information on the underlying implementation. + +_Note:_ Where OData and GraphQL are specifically required then they would need to be implemented separately as the Avanade accelerators do not provide this functionality out-of-the-box. + +
+ +### OData-like requirements + +A single `query` endpoint will be added to enable, and support the same requirements using OData-like filtering: + +Endpoint | Description +-|- +`GET /employees/query?$filter=startswith(lastName, 'smi*')` | all employees whose last name starts with `smi`. +`GET /employees/query?$filter=gender eq 'f'` | all female employees. +`GET /employees/query?$filter=startdate ge 2000-01-01 and startdate le 2002-12-31` | all employess who started between 01-Jan-2000 and 31-Dec-2002 (inclusive). +`GET /employees/query?$filter=startswith(lastName, 'smi*') and (terminated eq null || terminated ne null)` | all employees whose last name starts with `smi`, both current and terminated. +`GET /employees?gender=f&$skip=10&$take25` | all female employees with paging (skipping the first 10 employees and getting the next 25 in sequence). + +
+ +### Code-generation + +The following code-gen should be added after the `GetByArgs` configuration; this will add the `query` endpoint to the `Employee` entity. + +``` yaml + # Query operation + # - Type is GetColl that indicates that a collection is the expected result for the query-based operation. + # - ReturnType is overriding the default Employee as we want to use EmployeeBase (reduced set of fields). + # - Query indicates that OData-like $filter/$order support is to be enabled. + # - Paging indicates that paging support is required and to be automatically enabled for the operation. + # - WebApiRoute specifies the route of 'query' to be used for the operation. + { name: GetByQuery, type: GetColl, query: true, paging: true, returnType: EmployeeBase, webApiRoute: query } +``` + +Execute the code-generation again using the command line (within `MyEf.Hr.CodeGen` base directory) and the various artefacts will be updated (generated). + +### Data access logic + +The `EmployeeData.cs` partial class (non generated) will need to be extended to support the new `GetByQuery` operation. + +Firstly, the `QueryArgsConfig` must be instantiated with the desired configuration. This will explicitly add support for the specified fields to achieve the desired functionality: + +``` csharp +public partial class EmployeeData +{ + private static readonly QueryArgsConfig _config = QueryArgsConfig.Create() + .WithFilter(filter => filter + .AddField(nameof(Employee.LastName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddField(nameof(Employee.FirstName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddReferenceDataField(nameof(Employee.Gender), nameof(EfModel.Employee.GenderCode)) + .AddField(nameof(Employee.StartDate)) + .AddNullField(nameof(Employee.Termination), nameof(EfModel.Employee.TerminationDate), c => c.WithDefault(new QueryStatement($"{nameof(EfModel.Employee.TerminationDate)} == null")))) + .WithOrderBy(orderby => orderby + .AddField(nameof(Employee.LastName)) + .AddField(nameof(Employee.FirstName)) + .WithDefault($"{nameof(Employee.LastName)}, {nameof(Employee.FirstName)}")); +``` + +Secondly, the `GetByQuery` operation must be implemented. Add the following to the end of the `EmployeeDataCtor` method to enable the functionality: + +``` csharp + // Implement the GetByQuery OnQuery search/filtering logic using OData-like query syntax. + _getByQueryOnQuery = (q, args) => q.IgnoreAutoIncludes().Where(_config, args).OrderBy(_config, args); +``` + +
+ +### End-to-End testing + +Now that we've implemented `GetByQuery` search functionality, we can re-add the appropriate tests. Do so by un-commenting the region `GetByQuery` within `MyEf.Hr.Test/Apis/EmployeeTest.cs`. Within test explorer, run the `EmployeeTest` set of tests and confirm they all pass. + +
+ ## Next Step Next we will implement the [employee termination](./6-Employee-Terminate.md) endpoint. diff --git a/templates/Beef.Template.Solution/content/.template.config/template.json b/templates/Beef.Template.Solution/content/.template.config/template.json index 9691cbe1b..64646d6f6 100644 --- a/templates/Beef.Template.Solution/content/.template.config/template.json +++ b/templates/Beef.Template.Solution/content/.template.config/template.json @@ -85,7 +85,7 @@ "type": "generated", "generator": "constant", "parameters": { - "value": "3.24.1" + "value": "3.25.1" }, "replaces": "CoreExVersion" }, @@ -93,7 +93,7 @@ "type": "generated", "generator": "constant", "parameters": { - "value": "5.14.2" + "value": "5.15.0" }, "replaces": "BeefVersion" }, 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 7fafb915d..bcc87dd40 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs +++ b/templates/Beef.Template.Solution/content/Company.AppName.Api/Startup.cs @@ -52,7 +52,11 @@ public void ConfigureServices(IServiceCollection services) { var settings = sp.GetRequiredService(); var cco = new AzCosmos.CosmosClientOptions { SerializerOptions = new AzCosmos.CosmosSerializationOptions { PropertyNamingPolicy = AzCosmos.CosmosPropertyNamingPolicy.CamelCase, IgnoreNullValues = true } }; - return new AppNameCosmosDb(new AzCosmos.CosmosClient(settings.CosmosConnectionString, cco).GetDatabase(settings.CosmosDatabaseId), sp.GetRequiredService()); + return new AzCosmos.CosmosClient(settings.CosmosConnectionString, cco); + }).AddCosmosDb(sp => + { + var settings = sp.GetRequiredService(); + return new AppNameCosmosDb(sp.GetRequiredService().GetDatabase(settings.CosmosDatabaseId), sp.GetRequiredService()); }); #endif @@ -126,6 +130,7 @@ public void ConfigureServices(IServiceCollection services) options.SwaggerDoc("v1", new OpenApiInfo { Title = "Company.AppName API", Version = "v1" }); options.OperationFilter(); // Needed to support AcceptsBodyAttribute where body parameter not explicitly defined. options.OperationFilter(); // Needed to support PagingAttribute where PagingArgs parameter not explicitly defined. + options.OperationFilter(); // Needed to support QueryAttribute where QueryArgs parameter not explicitly defined. }); } diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Api/appsettings.json b/templates/Beef.Template.Solution/content/Company.AppName.Api/appsettings.json index 5086f9dfe..a24e164f0 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Api/appsettings.json +++ b/templates/Beef.Template.Solution/content/Company.AppName.Api/appsettings.json @@ -6,7 +6,7 @@ } }, //#if (implement_database || implement_sqlserver) - // Set using environment variable: 'AppName_ConnectionStrings__Database' + // Set using environment variable: 'AppName_ConnectionStrings__Database'. "ConnectionStrings": { //#if (implement_services) "ServiceBus": "Endpoint=sb://top-secret.servicebus.windows.net/;SharedAccessKeyName=top-secret;SharedAccessKey=top-encrypted-secret;EntityPath=event-stream", @@ -16,7 +16,7 @@ }, //#endif //#if (implement_mysql) - // Set using environment variable: 'AppName_ConnectionStrings__Database' + // Set using environment variable: 'AppName_ConnectionStrings__Database'. "ConnectionStrings": { //#if (implement_services) "ServiceBus": "Endpoint=sb://top-secret.servicebus.windows.net/;SharedAccessKeyName=top-secret;SharedAccessKey=top-encrypted-secret;EntityPath=event-stream", @@ -25,7 +25,7 @@ }, //#endif //#if (implement_postgres) - // Set using environment variable: 'AppName_ConnectionStrings__Database' + // Set using environment variable: 'AppName_ConnectionStrings__Database'. "ConnectionStrings": { //#if (implement_services) "ServiceBus": "Endpoint=sb://top-secret.servicebus.windows.net/;SharedAccessKeyName=top-secret;SharedAccessKey=top-encrypted-secret;EntityPath=event-stream", @@ -34,7 +34,7 @@ }, //#endif //#if (implement_cosmos) - // Set using environment variables: 'AppName_CosmosDb__ConnectionString' and 'AppName_CosmosDb__DatabaseId' (keeps values out of config file). + // Set using environment variables: 'AppName_CosmosConnectionString' and 'AppName_CosmosDatabaseId'. "CosmosConnectionString": "AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;AccountEndpoint=https://localhost:8081", "CosmosDatabaseId": "Company.AppName", //#endif diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Business/Company.AppName.Business.csproj b/templates/Beef.Template.Solution/content/Company.AppName.Business/Company.AppName.Business.csproj index 56572f1b1..805a8a08c 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Business/Company.AppName.Business.csproj +++ b/templates/Beef.Template.Solution/content/Company.AppName.Business/Company.AppName.Business.csproj @@ -24,13 +24,13 @@ - + - +
\ No newline at end of file diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Business/Data/PersonData.cs b/templates/Beef.Template.Solution/content/Company.AppName.Business/Data/PersonData.cs index e4e895d0d..9491eb424 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Business/Data/PersonData.cs +++ b/templates/Beef.Template.Solution/content/Company.AppName.Business/Data/PersonData.cs @@ -2,12 +2,31 @@ public partial class PersonData { +#if (implement_entityframework | implement_cosmos) + private static readonly QueryArgsConfig _config = QueryArgsConfig.Create() + .WithFilter(filter => filter + .AddField(nameof(Person.LastName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) + .AddField(nameof(Person.FirstName), c => c.WithOperators(QueryFilterOperator.AllStringOperators).WithUpperCase()) +#if (implement_entityframework) + .AddReferenceDataField(nameof(Person.Gender), nameof(EfModel.Person.GenderCode))) +#else + .AddReferenceDataField(nameof(Person.Gender))) +#endif + .WithOrderBy(orderby => orderby + .AddField(nameof(Person.LastName)) + .AddField(nameof(Person.FirstName)) + .WithDefault($"{nameof(Person.LastName)}, {nameof(Person.FirstName)}")); + +#endif /// /// Bind the implementation(s) to the corresponding extension(s) for runtime invocation. /// partial void PersonDataCtor() { _getByArgsOnQuery = GetByArgsOnQuery; +#if (implement_entityframework | implement_cosmos) + _getByQueryOnQuery = (q, args) => q.Where(_config, args).OrderBy(_config, args); +#endif } /// diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Business/GlobalUsings.cs b/templates/Beef.Template.Solution/content/Company.AppName.Business/GlobalUsings.cs index b9cbcbe2d..84aa09caf 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Business/GlobalUsings.cs +++ b/templates/Beef.Template.Solution/content/Company.AppName.Business/GlobalUsings.cs @@ -4,6 +4,9 @@ #if (implement_cosmos) global using CoreEx.Cosmos; #endif +#if (implement_entityframework | implement_cosmos) +global using CoreEx.Data.Querying; +#endif #if (implement_database || implement_entityframework) global using CoreEx.Database; #endif diff --git a/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/entity.beef-5.yaml b/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/entity.beef-5.yaml index d6773d6fb..f8d7718b9 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/entity.beef-5.yaml +++ b/templates/Beef.Template.Solution/content/Company.AppName.CodeGen/entity.beef-5.yaml @@ -50,8 +50,8 @@ etagDefaultMapperConverter: EncodedStringToUInt32Converter //#endif refDataText: true entities: - # The following is an example Entity with CRUD operations defined accessing a database using EntityFramework. -- { name: Person, collection: true, collectionResult: true, validator: PersonValidator, webApiRoutePrefix: persons, behavior: crupd, entityFrameworkModel: EfModel.Person, + # The following is an example Entity with CRUD and Query operations defined accessing a database using EntityFramework. +- { name: Person, collection: true, collectionResult: true, validator: PersonValidator, webApiRoutePrefix: persons, behavior: crupdq, entityFrameworkModel: EfModel.Person, properties: [ //#if (implement_sqlserver) { name: Id, type: Guid, primaryKey: true, dataName: PersonId }, @@ -84,7 +84,7 @@ entities: } //#endif //#if (implement_cosmos) -cosmosName: AppNameCosmosDb +cosmosType: AppNameCosmosDb eventSubjectRoot: Company eventActionFormat: PastTense eventSourceRoot: Company/AppName @@ -93,9 +93,9 @@ webApiAutoLocation: true autoImplement: Cosmos refDataText: true entities: - # The following is an example Entity with CRUD operations defined accessing a Cosmos DB. + # The following is an example Entity with CRUD and Query operations defined accessing a Cosmos DB. -- { name: Person, collection: true, collectionResult: true, validator: PersonValidator, identifierGenerator: true, webApiRoutePrefix: persons, behavior: crupd, cosmosContainerId: Persons, cosmosModel: Model.Person, +- { name: Person, collection: true, collectionResult: true, validator: PersonValidator, identifierGenerator: true, webApiRoutePrefix: persons, behavior: crupdq, cosmosContainerId: Persons, cosmosModel: Model.Person, properties: [ { name: Id, type: Guid, primaryKey: true, dataConverter: 'TypeToStringConverter' }, { name: FirstName }, @@ -129,7 +129,7 @@ eventSourceRoot: Company/AppName eventSourceKind: Relative webApiAutoLocation: true autoImplement: HttpAgent -httpAgentName: XxxAgent +httpAgentType: XxxAgent refDataText: true entities: # The following is an example Entity with CRUD operations defined accessing an HTTP endpoint. diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/FixtureSetup.cs b/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/FixtureSetup.cs index cf778eb1a..c60a29f29 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/FixtureSetup.cs +++ b/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/FixtureSetup.cs @@ -42,21 +42,32 @@ public void OneTimeSetUp() using var test = ApiTester.Create(); var cosmosDb = test.Services.GetRequiredService(); + // Create the Cosmos Db (where not exists). await cosmosDb.Database.Client.CreateDatabaseIfNotExistsAsync(cosmosDb.Database.Id, cancellationToken: ct).ConfigureAwait(false); - var ac = await cosmosDb.Database.ReplaceOrCreateContainerAsync(new AzCosmos.ContainerProperties - { - Id = cosmosDb.Persons.Container.Id, - PartitionKeyPath = "/_partitionKey" - }, 400, cancellationToken: ct).ConfigureAwait(false); + // Create 'Person' container. + var cdp = cosmosDb.Database.DefineContainer(cosmosDb.Persons.Container.Id, "/_partitionKey") + .WithIndexingPolicy() + .WithCompositeIndex() + .Path("/lastName", AzCosmos.CompositePathSortOrder.Ascending) + .Path("/firstName", AzCosmos.CompositePathSortOrder.Ascending) + .Attach() + .Attach() + .Build(); - var rdc = await cosmosDb.Database.ReplaceOrCreateContainerAsync(new AzCosmos.ContainerProperties - { - Id = "RefData", - PartitionKeyPath = "/_partitionKey", - UniqueKeyPolicy = new AzCosmos.UniqueKeyPolicy { UniqueKeys = { new AzCosmos.UniqueKey { Paths = { "/type", "/value/code" } } } } - }, 400, cancellationToken: ct).ConfigureAwait(false); + var ac = await cosmosDb.Database.ReplaceOrCreateContainerAsync(cdp, cancellationToken: ct).ConfigureAwait(false); + // Create 'RefData' container. + var cdr = cosmosDb.Database.DefineContainer("RefData", "/_partitionKey") + .WithUniqueKey() + .Path("/type") + .Path("/value/code") + .Attach() + .Build(); + + var rdc = await cosmosDb.Database.ReplaceOrCreateContainerAsync(cdr, cancellationToken: ct).ConfigureAwait(false); + + // Import the data. var jdr = JsonDataReader.ParseYaml("Person.yaml"); await cosmosDb.Persons.ImportBatchAsync(jdr, cancellationToken: ct).ConfigureAwait(false); diff --git a/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/PersonTest.cs b/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/PersonTest.cs index 9120049a9..07e211ff7 100644 --- a/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/PersonTest.cs +++ b/templates/Beef.Template.Solution/content/Company.AppName.Test/Apis/PersonTest.cs @@ -205,6 +205,114 @@ public void A280_GetByArgs_RefDataText() #endregion +#if (implement_entityframework | implement_cosmos) + #region GetByQuery + + [Test] + public void A310_GetByQuery_All() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(null)).Value!; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null.And.Count.EqualTo(4)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Jones", "Smith", "Smithers" })); + }); + } + + [Test] + public void A320_GetByQuery_Paging() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(null, PagingArgs.CreateSkipAndTake(1, 2))).Value!; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Jones", "Smith" })); + }); + } + + [Test] + public void A330_GetByQuery_FirstName() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("contains(firstname, 'a')"))).Value!; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null.And.Count.EqualTo(3)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Smith", "Smithers" })); + }); + } + + [Test] + public void A340_GetByQuery_LastName() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 's')"))).Value!; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Smith", "Smithers" })); + }); + } + + [Test] + public void A350_GetByQuery_Gender() + { + var v = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'f'"))).Value!; + + Assert.That(v, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(v!.Items, Is.Not.Null.And.Count.EqualTo(2)); + Assert.That(v.Items.Select(x => x.LastName).ToArray(), Is.EqualTo(new string[] { "Browne", "Jones" })); + }); + } + + [Test] + public void A360_GetByQuery_Empty() + { + Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("startswith(lastname, 's') and startswith(firstname, 'b') and gender eq 'f'"))) + .AssertJson("[]"); + } + + [Test] + public void A370_GetByQuery_FieldSelection() + { + Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'f'").Include("firstname", "lastname"))) + .AssertJson("[{\"firstName\":\"Rachael\",\"lastName\":\"Browne\"},{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}]"); + } + + [Test] + public void A880_GetByQuery_RefDataText() + { + var r = Agent() + .ExpectStatusCode(HttpStatusCode.OK) + .Run(a => a.GetByQueryAsync(QueryArgs.Create("gender eq 'f'"), requestOptions: new HttpRequestOptions { IncludeText = true })) + .AssertJsonFromResource("Person_A280_GetByArgs_Response.json", "etag", "changeLog"); + } + + #endregion + +#endif #region Create [Test] diff --git a/tests/Beef.Template.Solution.UnitTest/Beef.Template.Solution.UnitTest.csproj b/tests/Beef.Template.Solution.UnitTest/Beef.Template.Solution.UnitTest.csproj index 47ed20b8a..aaf8d76eb 100644 --- a/tests/Beef.Template.Solution.UnitTest/Beef.Template.Solution.UnitTest.csproj +++ b/tests/Beef.Template.Solution.UnitTest/Beef.Template.Solution.UnitTest.csproj @@ -6,12 +6,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs b/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs index cdddbcbfd..71779cdfb 100644 --- a/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs +++ b/tests/Beef.Template.Solution.UnitTest/TemplateTest.cs @@ -181,5 +181,5 @@ private static void SolutionCreateGenerateTest(string company, string appName, s if (services is not null) Assert.That(ExecuteCommand("dotnet", $"test {company}.{appName}.Services.Test.csproj", Path.Combine(dir, $"{company}.{appName}.Services.Test")).exitCode, Is.Zero, "dotnet test"); } - } + } } \ No newline at end of file diff --git a/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj b/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj index a4444fb1b..acea51d1a 100644 --- a/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj +++ b/tools/Beef.CodeGen.Core/Beef.CodeGen.Core.csproj @@ -34,7 +34,7 @@ - + diff --git a/tools/Beef.CodeGen.Core/CodeGenerator.cs b/tools/Beef.CodeGen.Core/CodeGenerator.cs index 8058dee83..050dee5fb 100644 --- a/tools/Beef.CodeGen.Core/CodeGenerator.cs +++ b/tools/Beef.CodeGen.Core/CodeGenerator.cs @@ -24,7 +24,7 @@ namespace Beef.CodeGen internal class CodeGenerator(ICodeGeneratorArgs args, CodeGenScript scripts) : OnRamp.CodeGenerator(args, scripts) { [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", Justification = "Supporting multi-versions of .NET.")] - private static readonly Regex _seeRegex = new(@"", RegexOptions.Compiled); + private static readonly Regex _seeRegex = new(@"", RegexOptions.Compiled); [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "SYSLIB1045:Convert to 'GeneratedRegexAttribute'.", Justification = "Supporting multi-versions of .NET.")] private static readonly Regex _seeRefRegex = new(@" \((.*?)\)", RegexOptions.Compiled); diff --git a/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs index 5330a7b35..bfe9160c2 100644 --- a/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs +++ b/tools/Beef.CodeGen.Core/Config/Entity/CodeGenConfig.cs @@ -227,10 +227,10 @@ public class CodeGenConfig : ConfigRootBase /// /// Gets or sets the default .NET database interface name used where `Operation.AutoImplement` is `Database`. /// - [JsonPropertyName("databaseName")] - [CodeGenProperty("Database", Title = "The .NET database interface name (used where `Operation.AutoImplement` is `Database`).", IsImportant = true, - Description = "Defaults to `IDatabase`. This can be overridden within the `Entity`(s).")] - public string? DatabaseName { get; set; } + [JsonPropertyName("databaseType")] + [CodeGenProperty("Database", Title = "The .NET database type and optional name (used where `Operation.AutoImplement` is `Database`).", IsImportant = true, + Description = "Defaults to `IDatabase`. Should be formatted as `Type` + `^` + `Name`; e.g. `IDatabase^Db`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).")] + public string? DatabaseType { get; set; } /// /// Gets or sets the default database schema name. @@ -259,34 +259,34 @@ public class CodeGenConfig : ConfigRootBase /// /// Gets or sets the default .NET Entity Framework interface name used where `Operation.AutoImplement` is `EntityFramework`. /// - [JsonPropertyName("entityFrameworkName")] - [CodeGenProperty("EntityFramework", Title = "The .NET Entity Framework interface name used where `Operation.AutoImplement` is `EntityFramework`.", - Description = "Defaults to `IEfDb`. This can be overridden within the `Entity`(s).")] - public string? EntityFrameworkName { get; set; } + [JsonPropertyName("entityFrameworkType")] + [CodeGenProperty("EntityFramework", Title = "The .NET Entity Framework type and optional name (used where `Operation.AutoImplement` is `EntityFramework`).", + Description = "Defaults to `IEfDb`. Should be formatted as `Type` + `^` + `Name`; e.g. `IEfDb^Ef`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).")] + public string? EntityFrameworkType { get; set; } /// /// Gets or sets the default .NET Cosmos interface name used where `Operation.AutoImplement` is `Cosmos`. /// - [JsonPropertyName("cosmosName")] - [CodeGenProperty("Cosmos", Title = "The .NET Entity Framework interface name used where `Operation.AutoImplement` is `Cosmos`.", IsImportant = true, - Description = "Defaults to `ICosmosDb`. This can be overridden within the `Entity`(s).")] - public string? CosmosName { get; set; } + [JsonPropertyName("cosmosType")] + [CodeGenProperty("Cosmos", Title = "The .NET Cosmos DB type and name (used where `Operation.AutoImplement` is `Cosmos`).", IsImportant = true, + Description = "Defaults to `ICosmosDb`. Should be formatted as `Type` + `^` + `Name`; e.g. `ICosmosDb^Cosmos`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).")] + public string? CosmosType { get; set; } /// /// Gets or sets the default .NET OData interface name used where `Operation.AutoImplement` is `OData`. /// - [JsonPropertyName("odataName")] + [JsonPropertyName("odataType")] [CodeGenProperty("OData", Title = "The .NET OData interface name used where `Operation.AutoImplement` is `OData`.", IsImportant = true, - Description = "Defaults to `IOData`. This can be overridden within the `Entity`(s).")] - public string? ODataName { get; set; } + Description = "Defaults to `IOData`. Should be formatted as `Type` + `^` + `Name`; e.g. `IOData^OData`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).")] + public string? ODataType { get; set; } /// /// Gets or sets the default .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`. /// - [JsonPropertyName("httpAgentName")] + [JsonPropertyName("httpAgentType")] [CodeGenProperty("HttpAgent", Title = "The default .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`.", IsImportant = true, - Description = "Defaults to `IHttpAgent`. This can be overridden within the `Entity`(s).")] - public string? HttpAgentName { get; set; } + Description = "Defaults to `IHttpAgent`. Should be formatted as `Type` + `^` + `Name`; e.g. `IHttpAgent^HttpAgent`. Where the `Name` portion is not specified it will be inferred. This can be overridden within the `Entity`(s).")] + public string? HttpAgentType { get; set; } /// /// Gets or sets the default ETag to/from RowVersion Mapping Converter used. @@ -595,6 +595,15 @@ protected override async Task PrepareAsync() NamespaceBusiness = DefaultWhereNull(NamespaceBusiness, () => $"{NamespaceBase}.Business"); NamespaceApi = DefaultWhereNull(NamespaceApi, () => $"{NamespaceBase}.{ApiName}"); + if (ExtraProperties is not null) + { + DatabaseType ??= ExtraProperties.Where(x => string.Compare(x.Key, "databaseName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + EntityFrameworkType ??= ExtraProperties.Where(x => string.Compare(x.Key, "entityFrameworkName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + CosmosType ??= ExtraProperties.Where(x => string.Compare(x.Key, "cosmosName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + ODataType ??= ExtraProperties.Where(x => string.Compare(x.Key, "odataName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + HttpAgentType ??= ExtraProperties.Where(x => string.Compare(x.Key, "httpAgentName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + } + WithResult = DefaultWhereNull(WithResult, () => true); PreprocessorDirectives = DefaultWhereNull(PreprocessorDirectives, () => false); ManagerCleanUp = DefaultWhereNull(ManagerCleanUp, () => false); @@ -609,12 +618,12 @@ protected override async Task PrepareAsync() AutoImplement = DefaultWhereNull(AutoImplement, () => "None"); DatabaseProvider = DefaultWhereNull(DatabaseProvider, () => "SqlServer"); DatabaseSchema = DefaultWhereNull(DatabaseSchema, () => DatabaseProvider == "SqlServer" ? "dbo" : ""); - DatabaseName = DefaultWhereNull(DatabaseName, () => "IDatabase"); + DatabaseType = DefaultWhereNull(DatabaseType, () => "IDatabase"); DatabaseMapperEx = DefaultWhereNull(DatabaseMapperEx, () => true); - EntityFrameworkName = DefaultWhereNull(EntityFrameworkName, () => "IEfDb"); - CosmosName = DefaultWhereNull(CosmosName, () => "ICosmosDb"); - ODataName = DefaultWhereNull(ODataName, () => "IOData"); - HttpAgentName = DefaultWhereNull(HttpAgentName, () => "IHttpAgent"); + EntityFrameworkType = DefaultWhereNull(EntityFrameworkType, () => "IEfDb"); + CosmosType = DefaultWhereNull(CosmosType, () => "ICosmosDb"); + ODataType = DefaultWhereNull(ODataType, () => "IOData"); + HttpAgentType = DefaultWhereNull(HttpAgentType, () => "IHttpAgent"); JsonSerializer = DefaultWhereNull(JsonSerializer, () => "SystemText"); ETagJsonName = DefaultWhereNull(ETagJsonName, () => "etag"); ETagDefaultMapperConverter = DefaultWhereNull(ETagDefaultMapperConverter, () => nameof(CoreEx.Mapping.Converters.StringToBase64Converter)); @@ -650,7 +659,7 @@ protected override async Task PrepareAsync() } } - // Check for any deprecate properties and warn. + // Check for any deprecated properties and warn/error. WarnWhereDeprecated(this, this, "refDataCache", "refDataAppendToNamespace", @@ -667,6 +676,13 @@ protected override async Task PrepareAsync() "eventOutbox", "eventSubjectFormat", "eventCasing"); + + WarnWhereDeprecated(this, this, + ("databaseName", " Please use 'databaseType' instead.", false), + ("entityFrameworkName", " Please use 'entityFrameworkType' instead.", false), + ("cosmosName", " Please use 'cosmosType' instead.", false), + ("odataName", " Please use 'odataType' instead.", false), + ("httpAgentName", " Please use 'httpAgentType' instead.", false)); } /// @@ -706,7 +722,7 @@ internal static void WarnWhereDeprecated(CodeGenConfig root, ConfigBase config, if (IsError) throw new CodeGenException(Property, $"Config [{config.BuildFullyQualifiedName(xp.Key)}] has been deprecated and is no longer supported.{(string.IsNullOrEmpty(Message) ? string.Empty : Message)}"); else - root.CodeGenArgs?.Logger?.LogWarning("{Deprecated}", $"Warning: Config [{config.BuildFullyQualifiedName(xp.Key)}] has been deprecated and will be ignored.{(string.IsNullOrEmpty(Message) ? string.Empty : Message)}"); + root.CodeGenArgs?.Logger?.LogWarning("{Deprecated}", $"Warning: Config [{config.BuildFullyQualifiedName(xp.Key)}] has been deprecated.{(string.IsNullOrEmpty(Message) ? string.Empty : Message)}"); } } } diff --git a/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs index d48f456c6..ad697c995 100644 --- a/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs +++ b/tools/Beef.CodeGen.Core/Config/Entity/EntityConfig.cs @@ -273,11 +273,11 @@ public class EntityConfig : ConfigBase #region Operation /// - /// Gets or sets the key CRUDBA behaviors (operations) will be automatically generated where not otherwise explicitly specified. + /// Gets or sets the key CRUDBAQ behaviors (operations) will be automatically generated where not otherwise explicitly specified. /// [JsonPropertyName("behavior")] - [CodeGenProperty("Operation", Title = "Defines the key CRUD-style behavior (operation types), being 'C'reate, 'G'et (or 'R'ead), 'U'pdate, 'P'atch and 'D'elete). Additionally, GetByArgs ('B') and GetAll ('A') operations that will be automatically generated where not otherwise explicitly specified.", - Description = "Value may only specifiy one or more of the `CGRUDBA` characters (in any order) to define the automatically generated behavior (operations); for example: `CRUPD` or `CRUP` or `rba` (case insensitive). " + + [CodeGenProperty("Operation", Title = "Defines the key CRUD-style behavior (operation types), being 'C'reate, 'G'et (or 'R'ead), 'U'pdate, 'P'atch, 'D'elete and `Q`uery). Additionally, `GetByArgs` ('B'), `GetAll` ('A') and `GetByQuery` ('Q') operations configuration will be automatically inferred where not otherwise explicitly specified.", + Description = "Value may only specifiy one or more of the `CGRUDBAQ` characters (in any order) to define the automatically generated behavior (operations); for example: `CRUPD` or `CRUP` or `rba` (case insensitive). " + "This is shorthand for setting one or more of the following properties: `Get`, `GetByArgs`, `GetAll`, 'Create', `Update`, `Patch` and `Delete`. Where one of these properties is set to either `true` or `false` this will take precedence over the value set for `Behavior`.")] public string? Behavior { get; set; } @@ -302,6 +302,13 @@ public class EntityConfig : ConfigBase [CodeGenProperty("Operation", Title = "Indicates that a `GetAll` operation will be automatically generated where not otherwise explicitly specified.")] public bool? GetAll { get; set; } + /// + /// Indicates that a `GetByQuery` operation will be automatically generated where not otherwise explicitly specified. + /// + [JsonPropertyName("getByQuery")] + [CodeGenProperty("Operation", Title = "Indicates that a `GetByQuery` operation will be automatically generated where not otherwise explicitly specified.")] + public bool? GetByQuery { get; set; } + /// /// Indicates that a `Create` operation will be automatically generated where not otherwise explicitly specified. /// @@ -439,10 +446,10 @@ public class EntityConfig : ConfigBase /// /// Gets or sets the .NET database interface name used where `AutoImplement` is `Database`. /// - [JsonPropertyName("databaseName")] - [CodeGenProperty("Database", Title = "The .NET database interface name (used where `AutoImplement` is `Database`).", IsImportant = true, - Description = "Defaults to the `CodeGeneration.DatabaseName` configuration property (its default value is `IDatabase`).")] - public string? DatabaseName { get; set; } + [JsonPropertyName("databaseType")] + [CodeGenProperty("Database", Title = "The .NET database type and optional name (used where `AutoImplement` is `Database`).", IsImportant = true, + Description = "Defaults to the `CodeGeneration.DatabaseName` configuration property (its default value is `IDatabase`). Should be formatted as `Type` + `^` + `Name`.")] + public string? DatabaseType { get; set; } /// /// Gets or sets the database schema name (used where `AutoImplement` is `Database`). @@ -482,10 +489,10 @@ public class EntityConfig : ConfigBase /// /// Gets or sets the .NET Entity Framework interface name used where `AutoImplement` is `EntityFramework`. /// - [JsonPropertyName("entityFrameworkName")] - [CodeGenProperty("EntityFramework", Title = "The .NET Entity Framework interface name used where `AutoImplement` is `EntityFramework`.", IsImportant = true, - Description = "Defaults to `CodeGeneration.EntityFrameworkName`.")] - public string? EntityFrameworkName { get; set; } + [JsonPropertyName("entityFrameworkType")] + [CodeGenProperty("EntityFramework", Title = "The .NET Entity Framework type and optyional name used where `AutoImplement` is `EntityFramework`.", IsImportant = true, + Description = "Defaults to `CodeGeneration.EntityFrameworkName`. Should be formatted as `Type` + `^` + `Name`.")] + public string? EntityFrameworkType { get; set; } /// /// Gets or sets the corresponding Entity Framework model name required where is EntityFramework. @@ -516,10 +523,10 @@ public class EntityConfig : ConfigBase /// /// Gets or sets the .NET Cosmos interface name used where `AutoImplement` is `Cosmos`. /// - [JsonPropertyName("cosmosName")] - [CodeGenProperty("Cosmos", Title = "The .NET Cosmos interface name used where `AutoImplement` is `Cosmos`.", IsImportant = true, - Description = "Defaults to the `CodeGeneration.CosmosName` configuration property (its default value is `ICosmosDb`).")] - public string? CosmosName { get; set; } + [JsonPropertyName("cosmosType")] + [CodeGenProperty("Cosmos", Title = "The .NET Cosmos DB type and optional name used where `AutoImplement` is `Cosmos`.", IsImportant = true, + Description = "Defaults to the `CodeGeneration.CosmosName` configuration property (its default value is `ICosmosDb`). Should be formatted as `Type` + `^` + `Name`.")] + public string? CosmosType { get; set; } /// /// Gets or sets the corresponding Cosmos model name required where is Cosmos. @@ -572,10 +579,10 @@ public class EntityConfig : ConfigBase /// /// Gets or sets the .NET OData interface name used where `AutoImplement` is `OData`. /// - [JsonPropertyName("odataName")] - [CodeGenProperty("OData", Title = "The .NET OData interface name used where `AutoImplement` is `OData`.", IsImportant = true, - Description = "Defaults to the `CodeGeneration.ODataName` configuration property (its default value is `IOData`).")] - public string? ODataName { get; set; } + [JsonPropertyName("odataType")] + [CodeGenProperty("OData", Title = "The .NET OData type and optional name used where `AutoImplement` is `OData`.", IsImportant = true, + Description = "Defaults to the `CodeGeneration.ODataName` configuration property (its default value is `IOData`). Should be formatted as `Type` + `^` + `Name`.")] + public string? ODataType { get; set; } /// /// Gets or sets the corresponding OData model name required where is OData. @@ -607,10 +614,10 @@ public class EntityConfig : ConfigBase /// /// Gets or sets the default .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`. /// - [JsonPropertyName("httpAgentName")] - [CodeGenProperty("HttpAgent", Title = "The .NET HTTP Agent interface name used where `Operation.AutoImplement` is `HttpAgent`.", IsImportant = true, - Description = "Defaults to `CodeGeneration.HttpAgentName` configuration property (its default value is `IHttpAgent`).")] - public string? HttpAgentName { get; set; } + [JsonPropertyName("httpAgentType")] + [CodeGenProperty("HttpAgent", Title = "The .NET HTTP Agent type and optional name used where `Operation.AutoImplement` is `HttpAgent`.", IsImportant = true, + Description = "Defaults to `CodeGeneration.HttpAgentName` configuration property (its default value is `IHttpAgent`). Should be formatted as `Type` + `^` + `Name`.")] + public string? HttpAgentType { get; set; } /// /// Gets or sets the HttpAgent API route prefix where `Operation.AutoImplement` is `HttpAgent`. @@ -1229,20 +1236,30 @@ public class EntityConfig : ConfigBase public List DataCtorParameters { get; } = []; /// - /// Gets the as a . + /// Gets the as a . /// public ParameterConfig? DatabaseDataParameter { get; set; } /// - /// Gets the as a . + /// Gets the as a . /// public ParameterConfig? EntityFrameworkDataParameter { get; set; } /// - /// Gets the as a . + /// Gets the as a . /// public ParameterConfig? CosmosDataParameter { get; set; } + /// + /// Gets the as a . + /// + public ParameterConfig? ODataDataParameter { get; set; } + + /// + /// Gets the as a . + /// + public ParameterConfig? HttpAgentDataParameter { get; set; } + /// /// Gets the EntityController collection. /// @@ -1456,6 +1473,15 @@ protected override async Task PrepareAsync() return words.Length > 1 && Parent!.Entities!.Any(x => x.Name == words[0]) ? string.Join(" ", new string[] { "{{" + words[0] + "}}" }.Concat(words[1..])) : string.Join(" ", words); })); + if (ExtraProperties is not null) + { + DatabaseType ??= ExtraProperties.Where(x => string.Compare(x.Key, "databaseName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + EntityFrameworkType ??= ExtraProperties.Where(x => string.Compare(x.Key, "entityFrameworkName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + CosmosType ??= ExtraProperties.Where(x => string.Compare(x.Key, "cosmosName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + ODataType ??= ExtraProperties.Where(x => string.Compare(x.Key, "odataName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + HttpAgentType ??= ExtraProperties.Where(x => string.Compare(x.Key, "httpAgentName", StringComparison.InvariantCultureIgnoreCase) == 0).Select(x => x.Value.ToString()).FirstOrDefault(); + } + FileName = DefaultWhereNull(FileName, () => Name); PrivateName = DefaultWhereNull(PrivateName, () => StringConverter.ToPrivateCase(Name)); ArgumentName = DefaultWhereNull(ArgumentName, () => StringConverter.ToCamelCase(Name)); @@ -1467,13 +1493,13 @@ protected override async Task PrepareAsync() JsonSerializer = DefaultWhereNull(JsonSerializer, () => Parent!.JsonSerializer); AutoImplement = DefaultWhereNull(AutoImplement, () => RefDataType is not null || EntityFrameworkModel is not null || CosmosModel is not null || ODataModel is not null || HttpAgentModel is not null ? Parent!.AutoImplement : "None"); DataCtor = DefaultWhereNull(DataCtor, () => "Public"); - DatabaseName = DefaultWhereNull(DatabaseName, () => Parent!.DatabaseName); + DatabaseType = DefaultWhereNull(DatabaseType, () => Parent!.DatabaseType); DatabaseSchema = DefaultWhereNull(DatabaseSchema, () => Parent!.DatabaseSchema); DatabaseMapperEx = DefaultWhereNull(DatabaseMapperEx, () => Parent!.DatabaseMapperEx); - EntityFrameworkName = DefaultWhereNull(EntityFrameworkName, () => Parent!.EntityFrameworkName); - CosmosName = DefaultWhereNull(CosmosName, () => Parent!.CosmosName); - ODataName = DefaultWhereNull(ODataName, () => Parent!.ODataName); - HttpAgentName = DefaultWhereNull(HttpAgentName, () => Parent!.HttpAgentName); + EntityFrameworkType = DefaultWhereNull(EntityFrameworkType, () => Parent!.EntityFrameworkType); + CosmosType = DefaultWhereNull(CosmosType, () => Parent!.CosmosType); + ODataType = DefaultWhereNull(ODataType, () => Parent!.ODataType); + HttpAgentType = DefaultWhereNull(HttpAgentType, () => Parent!.HttpAgentType); DataSvcCaching = DefaultWhereNull(DataSvcCaching, () => true); DataSvcCtor = DefaultWhereNull(DataSvcCtor, () => "Public"); DataSvcCustom = DefaultWhereNull(DataSvcCustom, () => "None"); @@ -1544,6 +1570,12 @@ protected override async Task PrepareAsync() else WebApiTags ??= []; + DatabaseDataParameter = CreateParameterConfigFromTypeAndOptionalName(DatabaseType, "Db"); + EntityFrameworkDataParameter = CreateParameterConfigFromTypeAndOptionalName(EntityFrameworkType, "Ef"); + CosmosDataParameter = CreateParameterConfigFromTypeAndOptionalName(CosmosType, "Cosmos"); + ODataDataParameter = CreateParameterConfigFromTypeAndOptionalName(ODataType, "OData"); + HttpAgentDataParameter = CreateParameterConfigFromTypeAndOptionalName(HttpAgentType, "HttpAgent"); + InferInherits(); Consts = await PrepareCollectionAsync(Consts).ConfigureAwait(false); await PreparePropertiesAsync().ConfigureAwait(false); @@ -1694,7 +1726,8 @@ private async Task PrepareOperationsAsync() case 'd': Delete = DefaultWhereNull(Delete, () => true); break; case 'b': GetByArgs = DefaultWhereNull(GetByArgs, () => true); break; case 'a': GetAll = DefaultWhereNull(GetAll, () => true); break; - default: throw new CodeGenException(this, nameof(Behavior), $"The '{c}' character does not map to a supported underlying behavior (operation) type; valid values are: 'CRGUPDBA' (case-insensitive)."); + case 'q': GetByQuery = DefaultWhereNull(GetByQuery, () => true); break; + default: throw new CodeGenException(this, nameof(Behavior), $"The '{c}' character does not map to a supported underlying behavior (operation) type; valid values are: 'CRGUPDBAQ' (case-insensitive)."); } } } @@ -1726,6 +1759,9 @@ private async Task PrepareOperationsAsync() if (CompareValue(GetAll, true) && !Operations.Any(x => x.Name == "GetAll")) Operations.Insert(0, new OperationConfig { Name = "GetAll", Type = "GetColl", WebApiRoute = GetByArgs is not null && GetByArgs.Value ? "all" : "" }); + if (CompareValue(GetByQuery, true) && !Operations.Any(x => x.Name == "GetByQuery")) + Operations.Insert(0, new OperationConfig { Name = "GetByQuery", Type = "GetColl", Paging = true, Query = true, WebApiRoute = "query" }); + // Prepare each operations. foreach (var operation in Operations) { @@ -1904,22 +1940,22 @@ private async Task PrepareConstructorsAsync() // Data constructors. if (UsesDatabase) - DataCtorParameters.Add(DatabaseDataParameter = new ParameterConfig { Name = "Db", Type = DatabaseName, Text = $"{{{{{DatabaseName}}}}}" }); + DataCtorParameters.Add(DatabaseDataParameter!); if (UsesEntityFramework) - DataCtorParameters.Add(EntityFrameworkDataParameter = new ParameterConfig { Name = "Ef", Type = EntityFrameworkName, Text = $"{{{{{EntityFrameworkName}}}}}" }); + DataCtorParameters.Add(EntityFrameworkDataParameter!); if (UsesCosmos) - DataCtorParameters.Add(CosmosDataParameter = new ParameterConfig { Name = "Cosmos", Type = CosmosName, Text = $"{{{{{CosmosName}}}}}" }); + DataCtorParameters.Add(CosmosDataParameter!); if (UsesOData) - DataCtorParameters.Add(new ParameterConfig { Name = "OData", Type = ODataName, Text = $"{{{{{ODataName}}}}}" }); + DataCtorParameters.Add(ODataDataParameter!); if (UsesHttpAgent) - DataCtorParameters.Add(new ParameterConfig { Name = "HttpAgent", Type = HttpAgentName, Text = $"{{{{{HttpAgentName}}}}}" }); + DataCtorParameters.Add(HttpAgentDataParameter!); if (SupportsDataEvents) - DataCtorParameters.Add(new ParameterConfig { Name = "Events", Type = $"IEventPublisher", Text = "{{IEventPublisher}}" }); + DataCtorParameters.Add(CreateParameterConfigFromTypeAndOptionalName("IEventPublisher", "Events")!); AddConfiguredParameters(DataCtorParams, DataCtorParameters); foreach (var ctor in DataCtorParameters) @@ -1949,7 +1985,7 @@ internal static void AddConfiguredParameters(List? configList, List x.Name == pc.Name)) paramList.Add(pc); } @@ -1958,8 +1994,11 @@ internal static void AddConfiguredParameters(List? configList, List /// Create parameter configuration from interface definition. /// - internal static ParameterConfig? CreateParameterConfigFromInterface(string text) + internal static ParameterConfig? CreateParameterConfigFromTypeAndOptionalName(string? text, string? nameOverride = null) { + if (text is null) + return null; + var parts = text.Split("^", StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return null; @@ -1967,14 +2006,21 @@ internal static void AddConfiguredParameters(List? configList, List", "", StringComparison.InvariantCulture); - if (pc.Name[0] == 'I' && pc.Name.Length > 1 && char.IsUpper(pc.Name[1])) - pc.Name = pc.Name[1..]; + if (!string.IsNullOrEmpty(nameOverride)) + pc.Name = nameOverride; + else + { + var nsparts = parts[0].Split(".", StringSplitOptions.RemoveEmptyEntries); + pc.Name = nsparts.Last().Replace("<", "", StringComparison.InvariantCulture).Replace(">", "", StringComparison.InvariantCulture); + if (pc.Name[0] == 'I' && pc.Name.Length > 1 && char.IsUpper(pc.Name[1])) + pc.Name = pc.Name[1..]; + } } else pc.Name = StringConverter.ToPascalCase(parts[1]); + pc.PrepareAsync(null!, null!).GetAwaiter().GetResult(); + return pc; } @@ -1984,7 +2030,7 @@ internal static void AddConfiguredParameters(List? configList, List CodeGenConfig.WarnWhereDeprecated(Root!, this, ("entityScope", null, false), ("entityUsing", null, false), - ("collectionKeyed", " Use the new 'collectionType' property with a value of 'Keyed' to achieve same functionality.", true), + ("collectionKeyed", " Please use 'collectionType' with a value of 'Keyed' to achieve same functionality.", true), ("refDataStringFormat", null, false), ("entityFrameworkMapperInheritsFrom", null, false), ("cosmosMapperInheritsFrom", null, false), @@ -1993,7 +2039,12 @@ private void CheckDeprecatedProperties() => CodeGenConfig.WarnWhereDeprecated(Ro ("eventSubjectFormat", null, false), ("eventCasing", null, false), ("iValidator", null, false), - ("crud", " Use the new 'behavior' property with a value of 'crupd' to achieve same functionality.", true), - ("databaseCustomerMapper", "Use 'databaseCustomMappper' instead; this was a spelling mistake that has now been corrected.", true)); + ("crud", " Please use 'behavior' with a value of 'crupd' to achieve same functionality.", true), + ("databaseCustomerMapper", "Please use 'databaseCustomMappper' instead; was a spelling mistake.", true), + ("databaseName", " Please use 'databaseType' instead.", false), + ("entityFrameworkName", " Please use 'entityFrameworkType' instead.", false), + ("cosmosName", " Please use 'cosmosType' instead.", false), + ("odataName", " Please use 'odataType' instead.", false), + ("httpAgentName", " Please use 'httpAgentType' instead.", false)); } } \ No newline at end of file diff --git a/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs index d12c459ce..a7c64b108 100644 --- a/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs +++ b/tools/Beef.CodeGen.Core/Config/Entity/OperationConfig.cs @@ -106,6 +106,13 @@ public class OperationConfig : ConfigBase [CodeGenProperty("Key", Title = "Indicates whether a `PagingArgs` argument is to be added to the operation to enable (standardized) paging related logic.", IsImportant = true)] public bool? Paging { get; set; } + /// + /// Indicates whether a QueryArgs argument is to be added to the operation to enable paging related logic. + /// + [JsonPropertyName("query")] + [CodeGenProperty("Key", Title = "Indicates whether a `QueryArgs` argument is to be added to the operation to enable OData-like $filter and $orderby related logic.", IsImportant = true)] + public bool? Query { get; set; } + /// /// Gets or sets the .NET value parameter for the operation. /// @@ -665,7 +672,7 @@ public class OperationConfig : ConfigBase /// /// Gets the without the paging parameter. /// - public List PagingLessDataParameters => DataParameters!.Where(x => CompareNullOrValue(x.IsPagingArgs, false)).ToList(); + public List PagingLessDataParameters => DataParameters!.Where(x => CompareNullOrValue(x.IsPagingArgs, false) && CompareNullOrValue(x.IsQueryArgs, false)).ToList(); /// /// Gets the without the value and paging parameters. @@ -705,17 +712,17 @@ public class OperationConfig : ConfigBase /// /// Gets the collection without the paging parameter. /// - public List? PagingLessParameters => Parameters!.Where(x => !x.IsPagingArgs).ToList(); + public List? PagingLessParameters => Parameters!.Where(x => !x.IsPagingArgs && !x.IsQueryArgs).ToList(); /// /// Gets the collection without the value and paging parameter. /// - public List? CoreParameters => ValueLessParameters!.Where(x => !x.IsPagingArgs).ToList(); + public List? CoreParameters => ValueLessParameters!.Where(x => !x.IsPagingArgs && !x.IsQueryArgs).ToList(); /// /// Gets the collection without parameters that do not need cleaning. /// - public List? CleanerParameters => Parameters!.Where(x => !x.LayerPassing!.StartsWith("ToManager", StringComparison.OrdinalIgnoreCase) && !x.IsPagingArgs && IsTrue(x.Parent!.ManagerCleanUp)).ToList(); + public List? CleanerParameters => Parameters!.Where(x => !x.LayerPassing!.StartsWith("ToManager", StringComparison.OrdinalIgnoreCase) && !x.IsPagingArgs && !x.IsQueryArgs && IsTrue(x.Parent!.ManagerCleanUp)).ToList(); /// /// Gets the collection for those parameters marked as ToManager*. @@ -727,6 +734,11 @@ public class OperationConfig : ConfigBase /// public ParameterConfig? PagingParameter => Parameters!.Where(x => x.IsPagingArgs).FirstOrDefault(); + /// + /// Gets the parameter that is . + /// + public ParameterConfig? QueryParameter => Parameters!.Where(x => x.IsQueryArgs).FirstOrDefault(); + /// /// Indicates whether there is full custom DataSvc-layer logic being invoked. /// @@ -1249,7 +1261,7 @@ protected override async Task PrepareAsync() { "GetColl" => "", "Custom" => "", - _ => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs).Select(x => $"{{{x.ArgumentName}}}")) + _ => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs && !x.IsQueryArgs).Select(x => $"{{{x.ArgumentName}}}")) }); if (!string.IsNullOrEmpty(WebApiRoute) && WebApiRoute.StartsWith('!')) @@ -1326,6 +1338,9 @@ private async Task PrepareParametersAsync() } } + if (Type == "GetColl" && CompareValue(Query, true)) + Parameters.Add(new ParameterConfig { Name = "Query", Type = "QueryArgs", Text = "{{QueryArgs}}", IsQueryArgs = true }); + if (Type == "GetColl" && CompareValue(Paging, true)) Parameters.Add(new ParameterConfig { Name = "Paging", Type = "PagingArgs", Text = "{{PagingArgs}}", IsPagingArgs = true }); @@ -1412,27 +1427,27 @@ private async Task PrepareDataAsync() switch (AutoImplement != "None" ? AutoImplement : Parent!.AutoImplement) { case "Database": - DataArgs.Name = "_db"; + DataArgs.Name = Parent?.DatabaseDataParameter?.PrivateName ?? "_db"; DataArgs.Type = "IDatabaseArgs"; break; case "EntityFramework": - DataArgs.Name = "_ef"; + DataArgs.Name = Parent?.EntityFrameworkDataParameter?.PrivateName ?? "_ef"; DataArgs.Type = "EfDbArgs"; break; case "Cosmos": DataArgs.Name = "_cosmos"; - DataArgs.Type = "CosmosDbArgs"; + DataArgs.Type = Parent?.CosmosDataParameter?.PrivateName ?? "CosmosDbArgs"; break; case "OData": DataArgs.Name = "_odata"; - DataArgs.Type = "ODataArgs"; + DataArgs.Type = Parent?.ODataDataParameter?.PrivateName ?? "ODataArgs"; break; case "HttpAgent": - DataArgs.Name = "_httpAgent"; + DataArgs.Name = Parent?.HttpAgentDataParameter?.PrivateName ?? "_httpAgent"; DataArgs.Type = null; break; @@ -1506,8 +1521,8 @@ private void PrepareHttpAgent() { "GetColl" => "", "Custom" => "", - "Update" => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs).Select(x => $"{{value.{x.Name}}}")), - _ => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs).Select(x => $"{{{x.ArgumentName}}}")) + "Update" => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs && !x.IsQueryArgs).Select(x => $"{{value.{x.Name}}}")), + _ => string.Join(",", Parameters!.Where(x => !x.IsValueArg && !x.IsPagingArgs && !x.IsQueryArgs).Select(x => $"{{{x.ArgumentName}}}")) }); if (!string.IsNullOrEmpty(HttpAgentRoute) && HttpAgentRoute.StartsWith('!')) diff --git a/tools/Beef.CodeGen.Core/Config/Entity/ParameterConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/ParameterConfig.cs index 307e3bdad..5215d867d 100644 --- a/tools/Beef.CodeGen.Core/Config/Entity/ParameterConfig.cs +++ b/tools/Beef.CodeGen.Core/Config/Entity/ParameterConfig.cs @@ -222,7 +222,12 @@ public class ParameterConfig : ConfigBase /// /// Indicates whether the parameter is the auto-enabled . /// - public bool IsPagingArgs { get; set; } + public bool IsPagingArgs { get; set; } + + /// + /// Indicates whether the parameter is the auto-enabled QueryArgs. + /// + public bool IsQueryArgs { get; set; } /// /// Gets the formatted summary text. @@ -294,7 +299,7 @@ protected override Task PrepareAsync() Nullable = true; } - RelatedEntity = Root!.Entities!.FirstOrDefault(x => x.Name == Type); + RelatedEntity = Root?.Entities!.FirstOrDefault(x => x.Name == Type); PrivateName = DefaultWhereNull(PrivateName, () => pc == null ? StringConverter.ToPrivateCase(Name) : pc.Name); ArgumentName = DefaultWhereNull(ArgumentName, () => pc == null ? StringConverter.ToCamelCase(Name) : pc.ArgumentName); @@ -304,7 +309,7 @@ protected override Task PrepareAsync() DataConverter = DefaultWhereNull(DataConverter, () => pc?.DataConverter); DataConverter = PropertyConfig.ReformatDataConverter(DataConverter, Type, RefDataType, null).DataConverter; WebApiFrom = DefaultWhereNull(WebApiFrom, () => RelatedEntity == null ? "FromQuery" : "FromEntityProperties"); - ValidationFramework = DefaultWhereNull(ValidationFramework, () => Parent!.ValidationFramework); + ValidationFramework = DefaultWhereNull(ValidationFramework, () => Parent?.ValidationFramework); RefDataType = DefaultWhereNull(RefDataType, () => pc?.RefDataType); if (Type!.StartsWith("^")) @@ -339,7 +344,7 @@ protected override Task PrepareAsync() if (string.IsNullOrEmpty(RefDataType) && !string.IsNullOrEmpty(pc?.Text)) return pc.Text; - if (IsValueArg || IsPagingArgs || Type == "ChangeLog") + if (IsValueArg || IsPagingArgs || IsQueryArgs || Type == "ChangeLog") return $"{StringConverter.ToSeeComments(Type)}"; if (RelatedEntity != null) diff --git a/tools/Beef.CodeGen.Core/Config/Entity/RefDataConfig.cs b/tools/Beef.CodeGen.Core/Config/Entity/RefDataConfig.cs index e016c102f..564e5aaca 100644 --- a/tools/Beef.CodeGen.Core/Config/Entity/RefDataConfig.cs +++ b/tools/Beef.CodeGen.Core/Config/Entity/RefDataConfig.cs @@ -47,16 +47,16 @@ protected override async Task PrepareAsync() // Data constructors. if (UsesDatabase) - DataCtorParameters.Add(new ParameterConfig { Name = "Db", Type = Root!.DatabaseName, Text = $"{{{{{Root!.DatabaseName}}}}}" }); + DataCtorParameters.Add(new ParameterConfig { Name = "Db", Type = Root!.DatabaseType, Text = $"{{{{{Root!.DatabaseType}}}}}" }); if (UsesEntityFramework) - DataCtorParameters.Add(new ParameterConfig { Name = "Ef", Type = Root!.EntityFrameworkName, Text = $"{{{{{Root!.EntityFrameworkName}}}}}" }); + DataCtorParameters.Add(new ParameterConfig { Name = "Ef", Type = Root!.EntityFrameworkType, Text = $"{{{{{Root!.EntityFrameworkType}}}}}" }); if (UsesCosmos) - DataCtorParameters.Add(new ParameterConfig { Name = "Cosmos", Type = Root!.CosmosName, Text = $"{{{{{Root!.CosmosName}}}}}" }); + DataCtorParameters.Add(new ParameterConfig { Name = "Cosmos", Type = Root!.CosmosType, Text = $"{{{{{Root!.CosmosType}}}}}" }); if (UsesOData) - DataCtorParameters.Add(new ParameterConfig { Name = "OData", Type = Root!.ODataName, Text = $"{{{{{Root!.ODataName}}}}}" }); + DataCtorParameters.Add(new ParameterConfig { Name = "OData", Type = Root!.ODataType, Text = $"{{{{{Root!.ODataType}}}}}" }); EntityConfig.AddConfiguredParameters(Root!.RefDataDataCtorParams, DataCtorParameters); diff --git a/tools/Beef.CodeGen.Core/Schema/entity.beef-5.json b/tools/Beef.CodeGen.Core/Schema/entity.beef-5.json index 72c984c5d..ff815de40 100644 --- a/tools/Beef.CodeGen.Core/Schema/entity.beef-5.json +++ b/tools/Beef.CodeGen.Core/Schema/entity.beef-5.json @@ -140,10 +140,10 @@ "None" ] }, - "databaseName": { + "databaseType": { "type": "string", - "title": "The .NET database interface name (used where \u0060Operation.AutoImplement\u0060 is \u0060Database\u0060).", - "description": "Defaults to \u0060IDatabase\u0060. This can be overridden within the \u0060Entity\u0060(s)." + "title": "The .NET database type and optional name (used where \u0060Operation.AutoImplement\u0060 is \u0060Database\u0060).", + "description": "Defaults to \u0060IDatabase\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060; e.g. \u0060IDatabase^Db\u0060. Where the \u0060Name\u0060 portion is not specified it will be inferred. This can be overridden within the \u0060Entity\u0060(s)." }, "databaseSchema": { "type": "string", @@ -165,25 +165,25 @@ "title": "Indicates that a \u0060DatabaseMapperEx\u0060 will be used; versus, \u0060DatabaseMapper\u0060 (which uses Reflection internally).", "description": "Defaults to \u0060true\u0060. The \u0060DatabaseMapperEx\u0060 essentially replaces the \u0060DatabaseMapper\u0060 as it is more performant (extended/explicit); this option can be used where leagcy/existing behavior is required." }, - "entityFrameworkName": { + "entityFrameworkType": { "type": "string", - "title": "The .NET Entity Framework interface name used where \u0060Operation.AutoImplement\u0060 is \u0060EntityFramework\u0060.", - "description": "Defaults to \u0060IEfDb\u0060. This can be overridden within the \u0060Entity\u0060(s)." + "title": "The .NET Entity Framework type and optional name (used where \u0060Operation.AutoImplement\u0060 is \u0060EntityFramework\u0060).", + "description": "Defaults to \u0060IEfDb\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060; e.g. \u0060IEfDb^Ef\u0060. Where the \u0060Name\u0060 portion is not specified it will be inferred. This can be overridden within the \u0060Entity\u0060(s)." }, - "cosmosName": { + "cosmosType": { "type": "string", - "title": "The .NET Entity Framework interface name used where \u0060Operation.AutoImplement\u0060 is \u0060Cosmos\u0060.", - "description": "Defaults to \u0060ICosmosDb\u0060. This can be overridden within the \u0060Entity\u0060(s)." + "title": "The .NET Cosmos DB type and name (used where \u0060Operation.AutoImplement\u0060 is \u0060Cosmos\u0060).", + "description": "Defaults to \u0060ICosmosDb\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060; e.g. \u0060ICosmosDb^Cosmos\u0060. Where the \u0060Name\u0060 portion is not specified it will be inferred. This can be overridden within the \u0060Entity\u0060(s)." }, - "odataName": { + "odataType": { "type": "string", "title": "The .NET OData interface name used where \u0060Operation.AutoImplement\u0060 is \u0060OData\u0060.", - "description": "Defaults to \u0060IOData\u0060. This can be overridden within the \u0060Entity\u0060(s)." + "description": "Defaults to \u0060IOData\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060; e.g. \u0060IOData^OData\u0060. Where the \u0060Name\u0060 portion is not specified it will be inferred. This can be overridden within the \u0060Entity\u0060(s)." }, - "httpAgentName": { + "httpAgentType": { "type": "string", "title": "The default .NET HTTP Agent interface name used where \u0060Operation.AutoImplement\u0060 is \u0060HttpAgent\u0060.", - "description": "Defaults to \u0060IHttpAgent\u0060. This can be overridden within the \u0060Entity\u0060(s)." + "description": "Defaults to \u0060IHttpAgent\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060; e.g. \u0060IHttpAgent^HttpAgent\u0060. Where the \u0060Name\u0060 portion is not specified it will be inferred. This can be overridden within the \u0060Entity\u0060(s)." }, "etagDefaultMapperConverter": { "type": "string", @@ -511,8 +511,8 @@ }, "behavior": { "type": "string", - "title": "Defines the key CRUD-style behavior (operation types), being \u0027C\u0027reate, \u0027G\u0027et (or \u0027R\u0027ead), \u0027U\u0027pdate, \u0027P\u0027atch and \u0027D\u0027elete). Additionally, GetByArgs (\u0027B\u0027) and GetAll (\u0027A\u0027) operations that will be automatically generated where not otherwise explicitly specified.", - "description": "Value may only specifiy one or more of the \u0060CGRUDBA\u0060 characters (in any order) to define the automatically generated behavior (operations); for example: \u0060CRUPD\u0060 or \u0060CRUP\u0060 or \u0060rba\u0060 (case insensitive). This is shorthand for setting one or more of the following properties: \u0060Get\u0060, \u0060GetByArgs\u0060, \u0060GetAll\u0060, \u0027Create\u0027, \u0060Update\u0060, \u0060Patch\u0060 and \u0060Delete\u0060. Where one of these properties is set to either \u0060true\u0060 or \u0060false\u0060 this will take precedence over the value set for \u0060Behavior\u0060." + "title": "Defines the key CRUD-style behavior (operation types), being \u0027C\u0027reate, \u0027G\u0027et (or \u0027R\u0027ead), \u0027U\u0027pdate, \u0027P\u0027atch, \u0027D\u0027elete and \u0060Q\u0060uery). Additionally, \u0060GetByArgs\u0060 (\u0027B\u0027), \u0060GetAll\u0060 (\u0027A\u0027) and \u0060GetByQuery\u0060 (\u0027Q\u0027) operations configuration will be automatically inferred where not otherwise explicitly specified.", + "description": "Value may only specifiy one or more of the \u0060CGRUDBAQ\u0060 characters (in any order) to define the automatically generated behavior (operations); for example: \u0060CRUPD\u0060 or \u0060CRUP\u0060 or \u0060rba\u0060 (case insensitive). This is shorthand for setting one or more of the following properties: \u0060Get\u0060, \u0060GetByArgs\u0060, \u0060GetAll\u0060, \u0027Create\u0027, \u0060Update\u0060, \u0060Patch\u0060 and \u0060Delete\u0060. Where one of these properties is set to either \u0060true\u0060 or \u0060false\u0060 this will take precedence over the value set for \u0060Behavior\u0060." }, "get": { "type": "boolean", @@ -526,6 +526,10 @@ "type": "boolean", "title": "Indicates that a \u0060GetAll\u0060 operation will be automatically generated where not otherwise explicitly specified." }, + "getByQuery": { + "type": "boolean", + "title": "Indicates that a \u0060GetByQuery\u0060 operation will be automatically generated where not otherwise explicitly specified." + }, "create": { "type": "boolean", "title": "Indicates that a \u0060Create\u0060 operation will be automatically generated where not otherwise explicitly specified." @@ -617,10 +621,10 @@ "title": "The Reference Data database stored procedure name.", "description": "Defaults to \u0060sp\u0060 (literal) \u002B \u0060Name\u0060 \u002B \u0060GetAll\u0060 (literal)." }, - "databaseName": { + "databaseType": { "type": "string", - "title": "The .NET database interface name (used where \u0060AutoImplement\u0060 is \u0060Database\u0060).", - "description": "Defaults to the \u0060CodeGeneration.DatabaseName\u0060 configuration property (its default value is \u0060IDatabase\u0060)." + "title": "The .NET database type and optional name (used where \u0060AutoImplement\u0060 is \u0060Database\u0060).", + "description": "Defaults to the \u0060CodeGeneration.DatabaseName\u0060 configuration property (its default value is \u0060IDatabase\u0060). Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060." }, "databaseSchema": { "type": "string", @@ -641,10 +645,10 @@ "title": "Indicates that a \u0060DatabaseMapperEx\u0060 (extended/explicit) will be used; versus, \u0060DatabaseMapper\u0060 (which uses Reflection internally).", "description": "Defaults to \u0060CodeGeneration.DatabaseMapperEx\u0060 (its default value is \u0060true\u0060). The \u0060DatabaseMapperEx\u0060 essentially replaces the \u0060DatabaseMapper\u0060 as it is more performant; this option can be used where leagcy/existing behavior is required." }, - "entityFrameworkName": { + "entityFrameworkType": { "type": "string", - "title": "The .NET Entity Framework interface name used where \u0060AutoImplement\u0060 is \u0060EntityFramework\u0060.", - "description": "Defaults to \u0060CodeGeneration.EntityFrameworkName\u0060." + "title": "The .NET Entity Framework type and optyional name used where \u0060AutoImplement\u0060 is \u0060EntityFramework\u0060.", + "description": "Defaults to \u0060CodeGeneration.EntityFrameworkName\u0060. Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060." }, "entityFrameworkModel": { "type": "string", @@ -659,10 +663,10 @@ "type": "string", "title": "The EntityFramework data-layer name that should be used for base mappings." }, - "cosmosName": { + "cosmosType": { "type": "string", - "title": "The .NET Cosmos interface name used where \u0060AutoImplement\u0060 is \u0060Cosmos\u0060.", - "description": "Defaults to the \u0060CodeGeneration.CosmosName\u0060 configuration property (its default value is \u0060ICosmosDb\u0060)." + "title": "The .NET Cosmos DB type and optional name used where \u0060AutoImplement\u0060 is \u0060Cosmos\u0060.", + "description": "Defaults to the \u0060CodeGeneration.CosmosName\u0060 configuration property (its default value is \u0060ICosmosDb\u0060). Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060." }, "cosmosModel": { "type": "string", @@ -690,10 +694,10 @@ "type": "string", "title": "The Cosmos data-layer name that should be used for base mappings." }, - "odataName": { + "odataType": { "type": "string", - "title": "The .NET OData interface name used where \u0060AutoImplement\u0060 is \u0060OData\u0060.", - "description": "Defaults to the \u0060CodeGeneration.ODataName\u0060 configuration property (its default value is \u0060IOData\u0060)." + "title": "The .NET OData type and optional name used where \u0060AutoImplement\u0060 is \u0060OData\u0060.", + "description": "Defaults to the \u0060CodeGeneration.ODataName\u0060 configuration property (its default value is \u0060IOData\u0060). Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060." }, "odataModel": { "type": "string", @@ -709,10 +713,10 @@ "title": "Indicates that a custom OData \u0060Mapper\u0060 will be used; i.e. not generated.", "description": "Otherwise, by default, a \u0060Mapper\u0060 will be generated." }, - "httpAgentName": { + "httpAgentType": { "type": "string", - "title": "The .NET HTTP Agent interface name used where \u0060Operation.AutoImplement\u0060 is \u0060HttpAgent\u0060.", - "description": "Defaults to \u0060CodeGeneration.HttpAgentName\u0060 configuration property (its default value is \u0060IHttpAgent\u0060)." + "title": "The .NET HTTP Agent type and optional name used where \u0060Operation.AutoImplement\u0060 is \u0060HttpAgent\u0060.", + "description": "Defaults to \u0060CodeGeneration.HttpAgentName\u0060 configuration property (its default value is \u0060IHttpAgent\u0060). Should be formatted as \u0060Type\u0060 \u002B \u0060^\u0060 \u002B \u0060Name\u0060." }, "httpAgentRoutePrefix": { "type": "string", @@ -1355,6 +1359,10 @@ "type": "boolean", "title": "Indicates whether a \u0060PagingArgs\u0060 argument is to be added to the operation to enable (standardized) paging related logic." }, + "query": { + "type": "boolean", + "title": "Indicates whether a \u0060QueryArgs\u0060 argument is to be added to the operation to enable OData-like $filter and $orderby related logic." + }, "valueType": { "type": "string", "title": "The .NET value parameter \u0060Type\u0060 for the operation.", diff --git a/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs index b2b38d838..b8d57a3e2 100644 --- a/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs +++ b/tools/Beef.CodeGen.Core/Templates/EntityData_cs.hbs @@ -426,7 +426,7 @@ public partial class {{Name}}Data{{#if GenericWithT}}{{/if}}{{#ifne Operation {{#unless CosmosCustomMapper}} /// - /// Provides the {{{EntityNameSeeComments}}} to Entity Framework {{{see-comments CosmosModel}}} mapping. + /// Provides the {{{EntityNameSeeComments}}} to Cosmos {{{see-comments CosmosModel}}} mapping. /// public partial class EntityToModelCosmosMapper : Mapper<{{EntityName}}, {{CosmosModel}}> { @@ -448,7 +448,7 @@ public partial class {{Name}}Data{{#if GenericWithT}}{{/if}}{{#ifne Operation } /// - /// Provides the Entity Framework {{{see-comments CosmosModel}}} to {{{EntityNameSeeComments}}} mapping. + /// Provides the Cosmos {{{see-comments CosmosModel}}} to {{{EntityNameSeeComments}}} mapping. /// public partial class ModelToEntityCosmosMapper : Mapper<{{CosmosModel}}, {{EntityName}}> { diff --git a/tools/Beef.CodeGen.Core/Templates/EntityIWebApiAgent_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityIWebApiAgent_cs.hbs index e98e70a20..8c5a045aa 100644 --- a/tools/Beef.CodeGen.Core/Templates/EntityIWebApiAgent_cs.hbs +++ b/tools/Beef.CodeGen.Core/Templates/EntityIWebApiAgent_cs.hbs @@ -46,7 +46,7 @@ namespace {{Root.NamespaceCommon}}.Agents /// The optional . /// The . /// A . - {{{AgentOperationTaskReturnType}}} {{Name}}Async({{#ifeq Type 'Patch'}}HttpPatchOption patchOption, {{/ifeq}}{{#each Parameters}}{{{WebApiAgentParameterType}}} {{ArgumentName}}{{#if IsPagingArgs}} = null{{/if}}, {{/each}}HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); + {{{AgentOperationTaskReturnType}}} {{Name}}Async({{#ifeq Type 'Patch'}}HttpPatchOption patchOption, {{/ifeq}}{{#each Parameters}}{{{WebApiAgentParameterType}}} {{ArgumentName}}{{#ifor IsPagingArgs IsQueryArgs}} = null{{/ifor}}, {{/each}}HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default); {{/each}} } }{{#if Root.PreprocessorDirectives}} diff --git a/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgent_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgent_cs.hbs index b4e86fdd9..725a177e4 100644 --- a/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgent_cs.hbs +++ b/tools/Beef.CodeGen.Core/Templates/EntityWebApiAgent_cs.hbs @@ -38,8 +38,8 @@ namespace {{Root.NamespaceCommon}}.Agents {{#each WebApiAgentOperations}} /// - public {{{AgentOperationTaskReturnType}}} {{Name}}Async({{#ifeq Type 'Patch'}}HttpPatchOption patchOption, {{/ifeq}}{{#each Parameters}}{{{WebApiAgentParameterType}}} {{ArgumentName}}{{#if IsPagingArgs}} = null{{/if}}, {{/each}}HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) - => {{AgentOperationHttpMethod}}("{{AgentWebApiRoute}}", {{#ifeq Type 'Patch'}}patchOption, {{/ifeq}}{{#if HasValue}}value, {{/if}}requestOptions: requestOptions{{#if Paging}}.IncludePaging(paging){{/if}}{{#ifne ValueLessParameters.Count 0}}, args: HttpArgs.Create({{#each CoreParameters}}{{#unless @first}}, {{/unless}}new HttpArg<{{{WebApiAgentParameterType}}}>("{{ArgumentName}}", {{ArgumentName}}{{#ifval WebApiAgentFrom}}, HttpArgType.{{WebApiAgentFrom}}{{/ifval}}){{/each}}){{/ifne}}, cancellationToken: cancellationToken); + public {{{AgentOperationTaskReturnType}}} {{Name}}Async({{#ifeq Type 'Patch'}}HttpPatchOption patchOption, {{/ifeq}}{{#each Parameters}}{{{WebApiAgentParameterType}}} {{ArgumentName}}{{#ifor IsPagingArgs IsQueryArgs}} = null{{/ifor}}, {{/each}}HttpRequestOptions? requestOptions = null, CancellationToken cancellationToken = default) + => {{AgentOperationHttpMethod}}("{{AgentWebApiRoute}}", {{#ifeq Type 'Patch'}}patchOption, {{/ifeq}}{{#if HasValue}}value, {{/if}}requestOptions: requestOptions{{#if Query}}.IncludeQuery(query){{/if}}{{#if Paging}}.IncludePaging(paging){{/if}}{{#ifne ValueLessParameters.Count 0}}, args: HttpArgs.Create({{#each CoreParameters}}{{#unless @first}}, {{/unless}}new HttpArg<{{{WebApiAgentParameterType}}}>("{{ArgumentName}}", {{ArgumentName}}{{#ifval WebApiAgentFrom}}, HttpArgType.{{WebApiAgentFrom}}{{/ifval}}){{/each}}){{/ifne}}, cancellationToken: cancellationToken); {{/each}} } }{{#if Root.PreprocessorDirectives}} diff --git a/tools/Beef.CodeGen.Core/Templates/EntityWebApiController_cs.hbs b/tools/Beef.CodeGen.Core/Templates/EntityWebApiController_cs.hbs index 57964ac59..9ebd79ff6 100644 --- a/tools/Beef.CodeGen.Core/Templates/EntityWebApiController_cs.hbs +++ b/tools/Beef.CodeGen.Core/Templates/EntityWebApiController_cs.hbs @@ -78,6 +78,9 @@ public partial class {{Name}}Controller : ControllerBase {{#if Paging}} [Paging] {{/if}} + {{#if Query}} + [Query] + {{/if}} {{#if HasValue}} [AcceptsBody(typeof({{CommonValueType}}){{#ifeq WebApiMethod 'HttpPatch'}}, HttpConsts.MergePatchMediaTypeName{{/ifeq}})] {{/if}} @@ -98,7 +101,7 @@ public partial class {{Name}}Controller : ControllerBase {{/ifeq}} {{/each}} {{#ifeq WebApiMethod 'HttpGet'}} - {{#if HasFromEntityPropertiesParameters}}return {{else}}=> {{/if}}_webApi.{{ControllerOperationWebApiMethod}}(Request, p => _manager.{{Name}}Async({{#each Parameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}p.Value!{{else}}{{#if IsPagingArgs}}p.RequestOptions.Paging{{else}}{{ArgumentName}}{{/if}}{{/if}}{{/each}}){{#ifne WebApiStatus 'OK'}}, statusCode: HttpStatusCode.{{WebApiStatus}}{{/ifne}}{{#if HasReturnValue}}{{#ifne WebApiAlternateStatus 'NotFound'}}, alternateStatusCode: HttpStatusCode.{{WebApiAlternateStatus}}{{/ifne}}{{/if}}{{#ifne ManagerOperationType 'Read'}}, operationType: CoreEx.OperationType.{{ManagerOperationType}}{{/ifne}}); + {{#if HasFromEntityPropertiesParameters}}return {{else}}=> {{/if}}_webApi.{{ControllerOperationWebApiMethod}}(Request, p => _manager.{{Name}}Async({{#each Parameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}p.Value!{{else}}{{#if IsQueryArgs}}p.RequestOptions.Query{{else}}{{#if IsPagingArgs}}p.RequestOptions.Paging{{else}}{{ArgumentName}}{{/if}}{{/if}}{{/if}}{{/each}}){{#ifne WebApiStatus 'OK'}}, statusCode: HttpStatusCode.{{WebApiStatus}}{{/ifne}}{{#if HasReturnValue}}{{#ifne WebApiAlternateStatus 'NotFound'}}, alternateStatusCode: HttpStatusCode.{{WebApiAlternateStatus}}{{/ifne}}{{/if}}{{#ifne ManagerOperationType 'Read'}}, operationType: CoreEx.OperationType.{{ManagerOperationType}}{{/ifne}}); {{/ifeq}} {{#ifeq WebApiMethod 'HttpPost'}} {{#if HasFromEntityPropertiesParameters}}return {{else}}=> {{/if}}_webApi.{{ControllerOperationWebApiMethod}}(Request, p => _manager.{{Name}}Async({{#each Parameters}}{{#unless @first}}, {{/unless}}{{#if IsValueArg}}p.Value!{{else}}{{#if IsPagingArgs}}p.RequestOptions.Paging{{else}}{{ArgumentName}}{{/if}}{{/if}}{{/each}}){{#ifne WebApiStatus 'OK'}}, statusCode: HttpStatusCode.{{WebApiStatus}}{{/ifne}}{{#if HasReturnValue}}{{#ifne WebApiAlternateStatus 'none'}}, alternateStatusCode: HttpStatusCode.{{WebApiAlternateStatus}}{{/ifne}}{{/if}}{{#ifne ManagerOperationType 'Create'}}, operationType: CoreEx.OperationType.{{ManagerOperationType}}{{/ifne}}{{#ifval WebApiLocation}}, locationUri: {{#if HasReturnValue}}r{{else}}(){{/if}} => new Uri($"{{WebApiLocation}}", UriKind.Relative){{/ifval}}); diff --git a/tools/Beef.Database.Core/Beef.Database.Core.csproj b/tools/Beef.Database.Core/Beef.Database.Core.csproj index bf717cdbc..bc06410cb 100644 --- a/tools/Beef.Database.Core/Beef.Database.Core.csproj +++ b/tools/Beef.Database.Core/Beef.Database.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/tools/Beef.Database.MySql/Beef.Database.MySql.csproj b/tools/Beef.Database.MySql/Beef.Database.MySql.csproj index f799a1be1..c4f06ad38 100644 --- a/tools/Beef.Database.MySql/Beef.Database.MySql.csproj +++ b/tools/Beef.Database.MySql/Beef.Database.MySql.csproj @@ -14,7 +14,7 @@ - + diff --git a/tools/Beef.Database.Postgres/Beef.Database.Postgres.csproj b/tools/Beef.Database.Postgres/Beef.Database.Postgres.csproj index d2f0f5dec..7a72d90a0 100644 --- a/tools/Beef.Database.Postgres/Beef.Database.Postgres.csproj +++ b/tools/Beef.Database.Postgres/Beef.Database.Postgres.csproj @@ -14,7 +14,7 @@ - + diff --git a/tools/Beef.Database.SqlServer/Beef.Database.SqlServer.csproj b/tools/Beef.Database.SqlServer/Beef.Database.SqlServer.csproj index 0d297684d..bf79956df 100644 --- a/tools/Beef.Database.SqlServer/Beef.Database.SqlServer.csproj +++ b/tools/Beef.Database.SqlServer/Beef.Database.SqlServer.csproj @@ -14,7 +14,7 @@ - + diff --git a/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj b/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj index aa93a64c0..c5f54363f 100644 --- a/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj +++ b/tools/Beef.Test.NUnit/Beef.Test.NUnit.csproj @@ -8,7 +8,7 @@ - +