-
Notifications
You must be signed in to change notification settings - Fork 14
Tutorial 02 14 Primary Key Factory Class
If you want your Harmony Core service to support creating entities via POST operations to your main collection endpoints, where the primary key values for new entities are generated on the server, you must add a custom primary key factory class, and you must tell Harmony Core about the class.
It is your job as a developer to provide whatever custom code is necessary to determine the appropriate primary key value for an entity being created. Frequently, as in the example that we will work with here, those primary key values are determined by reading the "next available" value from some application configuration file, but that is not a requirement.
The Harmony Core framework provides most of the "plumbing" code to make all of this happen. That code is provided in a class named Harmony.Core.FileIO.RecordPrimaryKeyFactory
. Your primary key factory method must inherit from that class and then provide a custom implementation for various methods.
The Harmony Core sample environment provides "next available" key values for all entity types via a simple relative file. The following is the initial data for that file:
Next customer number 000039
Next vendor_number 000046
Next order number 004725
Next item number 000122
But this data is provided as a text file. Later in this exercise you will add custom code to copy the file to a relative file each time the service starts. This guarantees that, as with the actual sample data files, the values of all "next available" keys will be the same each time the service starts.
- In Visual Studio Solution Explorer, right-click on the Services project and select
Add > Class
. - In the "Add New Item - Services" dialog, set the name of the new file to
PrimaryKeyGenerator.dbl
. Then click theAdd
button.
The new class should look like this:
import System
import System.Collections.Generic
import System.Text
namespace Services
public class PrimaryKeyGenerator
endclass
endnamespace
When you started this tutorial, you created your development solution based on a project template named harmonycore
that provides a pre-configured project hierarchy, but without any preexisting code. You may remember that there is also a project template named harmonydemo
that provides a fully implemented environment and includes a preconfigured primary key factory class. To save some time, you will be copying that same class into your project.
- Select and copy all the following code:
;;*****************************************************************************
;;
;; Title: PrimaryKeyGenerator.dbl
;;
;; Description: Provides primary key values for create (POST) operations.
;;
;;*****************************************************************************
;; THIS CODE WAS HANDCRAFTED
;;*****************************************************************************
import System
import System.Collections.Generic
import System.Text
import Harmony.Core
import Harmony.Core.FileIO
namespace Services
.include "SYSPARAMS" repository, structure="strSysParams", end
public class PrimaryKeyGenerator extends RecordPrimaryKeyFactory
mFileChannelManager, @IFileChannelManager
mChannel, int
mNextCustomerParam, strSysParams
mNextItemParam, strSysParams
mNextOrderParam, strSysParams
mNextVendorParam, strSysParams
public method PrimaryKeyGenerator
required in aFileChannelManager, @IFileChannelManager
proc
mFileChannelManager = aFileChannelManager
mChannel = mFileChannelManager.GetChannel("DAT:sysparams.ddf",FileOpenMode.UpdateRelative)
init mNextCustomerParam
init mNextItemParam
init mNextOrderParam
init mNextVendorParam
endmethod
protected override method IncrementKeyImplementation, @a
metaDatainstance, @DataObjectMetadataBase
proc
using metaDatainstance.RPSStructureName select
("CUSTOMERS"),
begin
if (!mNextCustomerParam.param_value)
read(mChannel,mNextCustomerParam,1,LOCK:Q_MANUAL_LOCK)
mNextCustomerParam.param_value += 1
mreturn (@a)%string(mNextCustomerParam.param_value-1)
end
("ITEMS"),
begin
if (!mNextItemParam.param_value)
read(mChannel,mNextItemParam,4,LOCK:Q_MANUAL_LOCK)
mNextItemParam.param_value += 1
mreturn (@a)%string(mNextItemParam.param_value-1)
end
("ORDERS"),
begin
if (!mNextOrderParam.param_value)
read(mChannel,mNextOrderParam,3,LOCK:Q_MANUAL_LOCK)
mNextOrderParam.param_value += 1
mreturn (@a)%string(mNextOrderParam.param_value-1)
end
("ORDER_ITEMS"),
begin
throw new NotImplementedException()
end
("VENDORS"),
begin
if (!mNextVendorParam.param_value)
read(mChannel,mNextVendorParam,2,LOCK:Q_MANUAL_LOCK)
mNextVendorParam.param_value += 1
mreturn (@a)%string(mNextVendorParam.param_value-1)
end
endusing
endmethod
protected override method CommitImplementation, void
proc
if (mNextCustomerParam.param_value)
begin
write(mChannel,mNextCustomerParam,1)
end
if (mNextItemParam.param_value)
begin
write(mChannel,mNextItemParam,4)
end
if (mNextOrderParam.param_value)
begin
write(mChannel,mNextOrderParam,3)
end
if (mNextVendorParam.param_value)
begin
write(mChannel,mNextVendorParam,2)
end
mFileChannelManager.ReturnChannel(mChannel)
mChannel = 0
endmethod
protected override method Abort, void
proc
mFileChannelManager.ReturnChannel(mChannel)
mChannel = 0
endmethod
endclass
endnamespace
-
In Visual Studio, select all the code in the new source file that you just added, and replace it with the code copied from this page.
-
Save the changes to the source file.
-
Right-click on the Services project, and select
Build
. Check the Output window to ensure that the build was successful.
1>------ Build started: Project: Services, Configuration: Debug Any CPU ------
========== Build: 1 succeeded, 0 failed, 4 up-to-date, 0 skipped ==========
Take a few minutes to examine the code that you just copied in and try to understand how it works. Here are the basics:
-
This is a class called
PrimaryKeyGenerator
that inherits functionality from the Harmony Core base classHarmony.Core.FileIO.RecordPrimaryKeyFactory
. -
Various private variables are declared to hold various values, including a channel number and the "next available" key values for our various entity types. In our sample environment, this information is stored in a relative file:
DAT:sysparams.ddf
. -
A constructor method
PrimaryKeyGenerator
is used to receive an instance of anIFileChannelManager
, which is a service that Harmony Core has made available via dependency injection and can be used to open channels to data files. After saving a reference to theIFileChannelManager
for use later, the constructor then uses it to open the data file that contains the various "next key value" records. The "next available" fields are also initialized to known (zero in this case) values.
The class has three other methods, all of which are overrides of methods in the base class that will be called by Harmony Core at specific times. However, before we describe those times, it is important that you understand the lifetime of instances of the class that you are looking at. An instance of this class is created for each transaction that occurs in the Entity Framework provider. In the case of a simple POST operation, that will mean that an instance of the class will be created for each operation, and the transaction for that operation will then be either committed or rolled-back if there is a problem. But in other scenarios, perhaps in custom code endpoints that might be added to a service, it is possible that a transaction could be used to create many different records of different types, and in that case, a single instance of this class would be used to service all the operations within that transaction.
-
The first override method is
IncrementKeyImplementation
. It is called whenever Harmony Core needs a primary key value for a new record being created via a POST operation. As you can see, the code receives aDataObjectMetadataBase
object, which contains, amongst other things, the name of the repository structure that is associated with the record that is being created. The code uses this information to determine which "next available" record to read from theDAT:sysparams.ddf
data file, reads that record, returns the "next available" number, and increments the value in memory. These updated values are not saved back to the data file until the end of the transaction, which is discussed below. -
The next override method is
CommitImplementation
. It is called whenever the transaction that the instance is associated with is being committed. The code saves any "next available" values that have been incremented during the current transaction back to theDAT:sysparams.ddf
data file, and then hands the channel to the file back to the channel manager service. -
The final override method is
Abort
. It is called if the transaction that the instance is associated with is rolled back. All this code does is hand the data file channel back to the channel manager.
Note: When a channel to a data file is handed back to the channel manager, the channel is not necessarily closed. Most likely it will be returned to a pool of available open channels that can be used again as needed to access the same file in the same mode.
In order for the custom primary key factory to become available and active in the environment at runtime, it is necessary to provide some custom code that will be executed as your Harmony Core service starts up.
Harmony Core has many extensibility points, and one of the patterns that is frequently used to enable you to plug in custom code is the use of partial classes and partial methods. You will use one of those extensibility points now. Essentially what you are about to do is add some custom code into the Startup.ConfigureServices
method, but without having to edit the source file that contains the actual method (Startup.dbl
) because that file is code-generated. If you were to edit the actual file, your changes would be lost the next time that code is re-generated for the project. By providing the custom code in a partial method in a partial class, you overcome that problem.
-
In the Visual Studio Solution Explorer, right-click on the Services project and select
Add > Class
. -
In the "Add New Item - Services" dialog, set the name of the new file to
StartupCustom.dbl
, and then click theAdd
button.
The new class should look like this:
import System
import System.Collections.Generic
import System.Text
namespace Services
public class StartupCustom
endclass
endnamespace
Depending on what you need to do in your custom startup code, you may need to use classes from many and varied namespaces, just like the code in the main Startup
class does. Because of this, it is generally best to start with the same collection of import statements that are used in the main part of the class.
-
Open
Startup.dbl
and copy all the import statements from near to the top of the file. -
Switch back to
StartupCustom.dbl
, select any existing import statements, and replace them with the import statements copied from the main class. -
Change the statement that declares the name of the class by adding the
partial
keyword, and change the name of the class toStartup
like this:public partial class Startup
-
Add the following code between the
class
andendclass
statements to declare the partial method:partial method ConfigureServicesCustom, void services, @Microsoft.Extensions.DependencyInjection.IServiceCollection proc endmethod
The final step is to tell Harmony Core about the existence of your custom primary key factory class so that it can start using it.
-
Finally, add the following code to the new partial method that you recently added:
services.AddScoped<IPrimaryKeyFactory,PrimaryKeyGenerator>()
The code in your source file should now look like this:
import Harmony.AspNetCore
import Harmony.AspNetCore.Context
import Harmony.Core
import Harmony.Core.Context
import Harmony.Core.FileIO
import Harmony.Core.Interface
import Harmony.Core.Utility
import Harmony.OData
import Harmony.OData.Adapter
import Microsoft.AspNetCore.Authorization
import Microsoft.AspNetCore.Authentication.JwtBearer
import Microsoft.AspNetCore.Builder
import Microsoft.AspNetCore.Hosting
import Microsoft.AspNetCore.Http
import Microsoft.AspNetCore.Mvc
import Microsoft.AspNetCore.Mvc.Abstractions
import Microsoft.AspNetCore.Mvc.ApiExplorer
import Microsoft.AspNetCore.StaticFiles
import Microsoft.AspNet.OData
import Microsoft.AspNet.OData.Extensions
import Microsoft.AspNet.OData.Builder
import Microsoft.AspNet.OData.Formatter
import Microsoft.AspNet.OData.Routing
import Microsoft.AspNet.OData.Routing.Conventions
import Microsoft.EntityFrameworkCore
import Microsoft.Extensions.Configuration
import Microsoft.Extensions.DependencyInjection
import Microsoft.Extensions.DependencyInjection.Extensions
import Microsoft.Extensions.Logging
import Microsoft.Extensions.Options
import Microsoft.Extensions.Primitives
import Microsoft.IdentityModel.Tokens
import Microsoft.Net.Http.Headers
import Microsoft.OData
import Microsoft.OData.Edm
import Microsoft.OData.UriParser
import System.Collections.Generic
import System.IO
import System.Linq
import System.Text
import System.Threading.Tasks
import Services.Controllers
import Services.Models
import Swashbuckle.AspNetCore.Swagger
import Microsoft.OpenApi.Models
namespace Services
public partial class Startup
partial method ConfigureServicesCustom, void
services, @IServiceCollection
proc
services.AddScoped<IPrimaryKeyFactory,PrimaryKeyGenerator>()
endmethod
endclass
endnamespace
-
Save the changes to the source file.
-
Right-click on the Services project and select
Build
. Check the Output window to ensure that the build was successful.
1>------ Build started: Project: Services, Configuration: Debug Any CPU ------
========== Build: 1 succeeded, 0 failed, 4 up-to-date, 0 skipped ==========
There is one final thing that you need to do before your primary key factory is complete, but this applies only to this sample environment. You would not want to do this in a production environment. The remaining task is to copy in a fresh copy of the "next available" data file each time the service starts. Remember that the code in the sample hosting environment does a similar thing for the actual entity data files.
-
Once again, open the
StartupCustom.dbl
source file that you recently added to the Services project. -
Add the following code to the procedure division of the existing
ConfigureServicesCustom
partial method.;;Create a new parameter file data parameterFile, string, "DAT:sysparams.ddf" data tmpChn = 0 try begin open(tmpChn,i:r,parameterFile) close tmpChn xcall delet(parameterFile) end catch (e, @NoFileFoundException) begin nop end finally begin data sourceFile = parameterFile.ToLower().Replace(".ddf",".txt") xcall copy(sourceFile,parameterFile,1) end endtry
-
Save the changes to the source file.
-
Right-click on the Services project and select
Build
. Check the Output window to ensure that the build was successful.1>------ Build started: Project: Services, Configuration: Debug Any CPU ------ ========== Build: 1 succeeded, 0 failed, 4 up-to-date, 0 skipped ==========
Your service now includes a custom primary key factory class that is capable of generating primary key values for the five data files that are exposed by your service. And it is plugged in and ready for use whenever POST requests are received.
Next topic: Adding Create Endpoints
-
Tutorial 2: Building a Service from Scratch
- Creating a Basic Solution
- Enabling OData Support
- Configuring Self Hosting
- Entity Collection Endpoints
- API Documentation
- Single Entity Endpoints
- OData Query Support
- Alternate Key Endpoints
- Expanding Relations
- Postman Tests
- Supporting CRUD Operations
- Adding a Primary Key Factory
- Adding Create Endpoints
- Adding Upsert Endpoints
- Adding Patch Endpoints
- Adding Delete Endpoints
-
Harmony Core Code Generator
-
OData Aware Tools
-
Advanced Topics
- CLI Tool Customization
- Adapters
- API Versioning
- Authentication
- Authorization
- Collection Counts
- Customization File
- Custom Field Types
- Custom File Specs
- Custom Properties
- Customizing Generated Code
- Deploying to Linux
- Dynamic Call Protocol
- Environment Variables
- Field Security
- File I/O
- Improving AppSettings Processing
- Logging
- Optimistic Concurrency
- Multi-Tenancy
- Publishing in IIS
- Repeatable Unit Tests
- Stored Procedure Routing
- Suppressing OData Metadata
- Traditional Bridge
- Unit Testing
- EF Core Optimization
- Updating a Harmony Core Solution
- Updating to 3.1.90
- Creating a new Release
-
Background Information