Skip to content

Tutorial 02 14 Primary Key Factory Class

mattl91 edited this page Feb 16, 2023 · 12 revisions

Harmony Core Logo

Adding a Primary Key Factory

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.

VIDEO: Creating a Basic Solution

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 Sample Environment

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.

Adding a Primary Key Factory Class

  1. In Visual Studio Solution Explorer, right-click on the Services project and select Add > Class.
  2. In the "Add New Item - Services" dialog, set the name of the new file to PrimaryKeyGenerator.dbl. Then click the Add button.

The new class should look like this:

import System
import System.Collections.Generic
import System.Text

namespace Services

    public class PrimaryKeyGenerator
	
    endclass

endnamespace

Copying in Example Code

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.

  1. 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
  1. 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.

  2. Save the changes to the source file.

  3. 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 ==========

Examining the Code

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 class Harmony.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 an IFileChannelManager, 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 the IFileChannelManager 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 a DataObjectMetadataBase 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 the DAT: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 the DAT: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.

Making the Code Available as a Service

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.

  1. In the Visual Studio Solution Explorer, right-click on the Services project and select Add > Class.

  2. In the "Add New Item - Services" dialog, set the name of the new file to StartupCustom.dbl, and then click the Add 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.

  1. Open Startup.dbl and copy all the import statements from near to the top of the file.

  2. Switch back to StartupCustom.dbl, select any existing import statements, and replace them with the import statements copied from the main class.

  3. Change the statement that declares the name of the class by adding the partial keyword, and change the name of the class to Startup like this:

    public partial class Startup
    
  4. Add the following code between the class and endclass 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.

  1. 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
  1. Save the changes to the source file.

  2. 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 ==========

Adding Custom Code to Provide the Relative File

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.

  1. Once again, open the StartupCustom.dbl source file that you recently added to the Services project.

  2. 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
    
  3. Save the changes to the source file.

  4. 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 ==========
    

Review

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


Clone this wiki locally