Sunday, 8 December 2024

Should naming procedures Setup and Teardown be reserved for unit testing?

I recently came across some code a developer had done where they had named 2 public procedures 'Setup' and 'Teardown', when I fist saw these I instinctively thought of unit tests and thought that class might relate to unit testing. After looking into the code it was apparent that these procedures where meant to be called after an object of the class was created and before the object was freed, but had nothing to do with unit testing.

Even though there are no restrictions on using these words for procedures I do think it is best practice not to use them in production code and name them something different like 'InitializeResources' and 'CleanupResources'. I think keeping procedures 'Setup' and 'Teardown' specifically for testing maintains a clear distinction between testing and application logic, which can be beneficial for maintainability.

Wednesday, 24 July 2024

Why change how a Web Broker service works?

I have a web service (web broker) that works fine and each request is sent to a corresponding object that relates to the request and the response is returned, for example the /customerinfo request (GET) calls an object that is private to the TWebModule that returns customer information.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    Response.Content := FCustomers.CustomerInfo(Request);
end; 

BTW, this is not the actual code, but is just an example. This works fine and has been working fine for some time. However, another developer has come up with what they consider a better way fo doing this. First, they add a property of TWebRequest to all objects that use TWebRequest, so in this example the FCustomer object has a WebRequest property. Then in the BeforeDispatch set the WebRequest property of all the web module objects (not just the one that relates to the request) to the web request. The advantage of this is so that you do not need to pass in the WebRequest in the action e.g.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    Response.Content := FCustomers.CustomerInfo;
end; 

They have also proposed that we could also set the TWebResponse on all the objects to that then all you need to do is this.

procedure TMyWebModule.CustomerInfoAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
    FCustomers.CustomerInfo;
end; 

The other change that is being proposed is to add validation to the TWebRequest, by having a new class that descends from TWebRequest, lets call it TMyWebRequest and this will have validation code, so that you can call something like MyWebRequest.Validate it will do some validation. Currently there is a validation class that takes the TWebRequest, but this would be removed and the TMyWebRequest would validate itself.

In the AfterDispatch method it will iterate through all the objects and set the MyWebRequest property to nil.

There are a few things I am not keen about with making these changes. 

  • I don't like the idea of descending from TWebRequest just to add some validation methods, and much prefer having validation classes that are responsible for doing the validation.
  • Currently passing TWebRequest makes it clear and explicit that the parameter is expected, making it easier to trace.
  • The current way keeps the responsibility of handling the request and response with the action.
  • It is setting the MyWebRequest property of all the objects, even though it is only required for the object that will be called, for example it sets the MyWebRequest property on the Order, Product, System and Customer objects even though it is only the Customer object that will be called with that requrest.
  • It means that a developer can use both methods, this could cause confusion.
  • It will take some time to change all the services to work this new way. 

The benefits I can see from doing this are:

  • Reduce the number of parameters passed.
I cannot see any real benefits from making these changes, or am I missing something?


Friday, 24 November 2023

How to write try finally blocks

 I recently came across code another developer had done and they write try..finally blocks like the following:

procedure TSomeClass.DoSomething: string;
var
    myClass: TMyClass;
begin
    myClass := nil;
    try
        myClass := TMyClass.Create;
        // Do stuff
    finally
        myClass.Free;
    end;
end;

Were I would write it as follows:

procedure TSomeClass.DoSomething: string;
var
    myClass: TMyClass;
begin
    myClass := TMyClass.Create;
    try
        // Do stuff
    finally
        myClass.Free;
    end;
end;

I always create the object on the line before the try and would not set the object to nil just before creating it. I believe setting the object to nil before creating it and putting the create after the try to not be the correct way and do not know the reasoning why the developer does it like this.


Saturday, 12 February 2022

Writing to and reading from the windows event Log

I've written some classes that writes to and reads from the Windows event log. The TRBWindowsEventLogs class contains the writer and reader objects, when creating the object from this class it requires the application name to be passed, this is can be different is required to the application, but is also used to retrieve the log entries.

unit uWindowsEvents;

interface

uses classes, Windows, SvcMgr, Vcl.StdCtrls, Generics.Collections;

type

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEvent = class(TObject)
  strict private
    fRecordNumber: integer;
    fMessage: string;
    fComputerName: string;
    fEventData: string;
    fLogFile: string;
    fCategory: string;
    fEventCode: integer;
  public
    property Category: string read fCategory write fCategory;
    property ComputerName: string read fComputerName write fComputerName;
    property EventCode: integer read fEventCode write fEventCode;
    property Message: string read fMessage write fMessage;
    property RecordNumber: integer read fRecordNumber write fRecordNumber;
    property LogFile: string read fLogFile write fLogFile;
    property EventData: string read fEventData write fEventData;

    procedure Populate(aEvent: OLEVariant);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  IRBEventReaderOutput = interface
    ['{4ADA872A-C1DA-4B3B-BE67-0F628C61039C}']
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogsReader = class(TObjectList<TRBWindowsEvent>)
  strict private
    fApplicationName: string;
    fMaxNumberOfEntries: integer;

    function EventQuery: string;

    procedure GetWindowsEventLogs;
    procedure AddErrorMessage(aErrorMessage: string);
  public
    property MaxNumberOfEntries: integer read fMaxNumberOfEntries write fMaxNumberOfEntries;

    constructor Create(aApplicationName: string; aMaxNumberOfEntries: integer);

    procedure PopulateEvents(aReaderOutput: IRBEventReaderOutput);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogsWriter = class(TObject)
  strict private
    fWindowsEventLogger: TEventLogger;
  public
    constructor Create(aApplicationName: string);
    destructor Destroy; override;

    procedure WriteInformationToWindowsEvents(aMessage: string);
    procedure WriteErrorToWindowsEvents(aMessage: string);
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TRBWindowsEventLogs = class(TObject)
  strict private
    fApplicationName: string;

    fWriter: TRBWindowsEventLogsWriter;
    fReader: TRBWindowsEventLogsReader;

  public
    property Writer: TRBWindowsEventLogsWriter read fWriter;
    property Reader: TRBWindowsEventLogsReader read fReader;

    constructor Create(aApplicationName: string); overload;
    constructor Create(aApplicationName: string; aMaxNumberOfEntries: integer); overload;
    destructor Destroy; override;
  end;

implementation

uses SysUtils, ComObj, ActiveX, System.Variants, DateUtils;

{ TRBWindowsEvent }

procedure TRBWindowsEvent.Populate(aEvent: OLEVariant);
var
  insertion: array of String;
  i: integer;
begin
  fCategory := string(aEvent.Category);
  fComputerName := string(aEvent.ComputerName);
  fEventCode := integer(aEvent.EventCode);
  fMessage := string(aEvent.Message);
  fRecordNumber := integer(aEvent.RecordNumber);
  fLogFile := string(aEvent.LogFile);

  if not VarIsNull(aEvent.InsertionStrings) then
  begin
    insertion := aEvent.InsertionStrings;
    for i := VarArrayLowBound(insertion, 1) to VarArrayHighBound(insertion, 1) do
    begin
      fEventData := fEventData + insertion[i];
    end;
  end;
end;

{ TRBWindowsEvents }

constructor TRBWindowsEventLogs.Create(aApplicationName: string);
begin
  Create(aApplicationName, 100);
end;

constructor TRBWindowsEventLogs.Create(aApplicationName: string; aMaxNumberOfEntries: integer);
begin
  inherited Create;
  fApplicationName := aApplicationName;
  fWriter := TRBWindowsEventLogsWriter.Create(aApplicationName);
  fReader := TRBWindowsEventLogsReader.Create(aApplicationName, aMaxNumberOfEntries);
end;

destructor TRBWindowsEventLogs.Destroy;
begin
  FreeAndNil(fReader);
  FreeAndNil(fWriter);
  inherited;
end;

{ TRBWindowsEventLogsWriter }

constructor TRBWindowsEventLogsWriter.Create(aApplicationName: string);
begin
  inherited Create;
  fWindowsEventLogger := TEventLogger.Create(aApplicationName);
end;

destructor TRBWindowsEventLogsWriter.Destroy;
begin
  FreeAndNil(fWindowsEventLogger);
  inherited;
end;

procedure TRBWindowsEventLogsWriter.WriteErrorToWindowsEvents(aMessage: string);
begin
  fWindowsEventLogger.LogMessage(aMessage, EVENTLOG_ERROR_TYPE);
end;

procedure TRBWindowsEventLogsWriter.WriteInformationToWindowsEvents(aMessage: string);
begin
  fWindowsEventLogger.LogMessage(aMessage, EVENTLOG_INFORMATION_TYPE);
end;

{ TRBWindowsEventLogsReader }

constructor TRBWindowsEventLogsReader.Create(aApplicationName: string; aMaxNumberOfEntries: integer);
begin
  inherited Create;
  fApplicationName := aApplicationName;
  fMaxNumberOfEntries := aMaxNumberOfEntries;
end;

function TRBWindowsEventLogsReader.EventQuery: string;
begin
  Result := 'SELECT * FROM Win32_NTLogEvent Where SourceName = "' + fApplicationName +
    '" AND Logfile = "Application" AND TimeGenerated >= "' + DateTimeToStr(IncDay(Now(), -1)) + '"';
end;

procedure TRBWindowsEventLogsReader.AddErrorMessage(aErrorMessage: string);
var
  event: TRBWindowsEvent;
begin
  event := TRBWindowsEvent.Create;
  event.Category := 'Error';
  event.Message := aErrorMessage;
  Add(event);
end;

procedure TRBWindowsEventLogsReader.GetWindowsEventLogs;
const
  wbemForwardOnly = 32;
  wbemReturnImmediately = 16;
var
  SWbemLocator: OLEVariant;
  WMIService: OLEVariant;
  WbemObjectSet: OLEVariant;
  WbemObject: OLEVariant;
  oEnum: IEnumvariant;
  iValue: LongWord;
  iCount: integer;
  event: TRBWindowsEvent;
begin
  try
    Clear;
    iCount := 0;
    SWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
    WMIService := SWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', '');
    WbemObjectSet := WMIService.ExecQuery(EventQuery(), 'WQL', wbemReturnImmediately + wbemForwardOnly);
    oEnum := IUnknown(WbemObjectSet._NewEnum) as IEnumvariant;
    while oEnum.Next(1, WbemObject, iValue) = 0 do
    begin
      event := TRBWindowsEvent.Create;
      event.Populate(WbemObject);
      Add(event);
      WbemObject := Unassigned;
      inc(iCount);
      if iCount > fMaxNumberOfEntries then
      begin
        Break;
      end;
    end;
  except
    on E: EOleException do
      AddErrorMessage(Format('EOleException %s %x', [E.Message, E.ErrorCode]));
    on E: Exception do
      AddErrorMessage(E.Classname + ':' + E.Message);
  end;
end;

procedure TRBWindowsEventLogsReader.PopulateEvents(aReaderOutput: IRBEventReaderOutput);
var
  event: TRBWindowsEvent;
begin
  GetWindowsEventLogs;
  for event in Self do
  begin
    aReaderOutput.AddEventLog(event);
  end;
end;

end.


To use these classes I've created some output classes implemented from the IRBEventReaderOutput interface.

unit uWindowsEventsOutput;

interface

uses classes, Windows, uWindowsEvents, System.JSON;

type
  { /----------------------------------------------------------------------------------------------------------------- }
  TStringsReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fStrings: TStringList;
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;

    constructor Create;
    destructor Destroy; override;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TJSONReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fJSON_Array: TJSONArray;
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;

    constructor Create;
    destructor Destroy; override;
  end;

  { /----------------------------------------------------------------------------------------------------------------- }
  TCSVReaderOutput = class(TInterfacedObject, IRBEventReaderOutput)
  strict private
    fCSVString: string;
    procedure AddHeader;
    procedure AddLine(aLine: string);
  public
    procedure AddEventLog(aEventLog: TRBWindowsEvent);
    procedure SaveToFile(aFileName: string);
    function OutputString: string;
  end;

implementation

uses SysUtils;

{ TStringsReaderOutput }

constructor TStringsReaderOutput.Create;
begin
  inherited Create;
  fStrings := TStringList.Create;
end;

destructor TStringsReaderOutput.Destroy;
begin
  FreeAndNil(fStrings);
  inherited;
end;

function TStringsReaderOutput.OutputString: string;
begin
  Result := fStrings.CommaText;
end;

procedure TStringsReaderOutput.SaveToFile(aFileName: string);
begin
  fStrings.SaveToFile(aFileName);
end;

procedure TStringsReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
begin
  fStrings.Add('Category: ' + aEventLog.Category);
  fStrings.Add('Computer Name: ' + aEventLog.ComputerName);
  fStrings.Add('Event Code: ' + aEventLog.EventCode.ToString);
  fStrings.Add('Message: ' + aEventLog.Message);
  fStrings.Add('Record Number: ' + aEventLog.RecordNumber.ToString);
  fStrings.Add('Log File: ' + aEventLog.LogFile);
  fStrings.Add('Event Data');
  fStrings.Add(aEventLog.EventData);
  fStrings.Add('-------------------------');
end;


{ TJSONReaderOutput }

constructor TJSONReaderOutput.Create;
begin
  inherited Create;
  fJSON_Array := TJSONArray.Create;
end;

destructor TJSONReaderOutput.Destroy;
begin
  FreeAndNil(fJSON_Array);
  inherited;
end;

function TJSONReaderOutput.OutputString: string;
begin
  if Assigned(fJSON_Array) then
  begin
    Result := fJSON_Array.ToJSON;
  end;
end;

procedure TJSONReaderOutput.SaveToFile(aFileName: string);
var
  sl: TStringList;
begin
  sl := TStringList.Create;
  try
    sl.Text := fJSON_Array.ToJSON;
    sl.SaveToFile(aFileName);
  finally
    sl.Free;
  end;
end;

procedure TJSONReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
var
  JSON_Object: TJSONObject;
begin
  JSON_Object := TJSONObject.Create;
  JSON_Object.AddPair('category', aEventLog.Category);
  JSON_Object.AddPair('computerName', aEventLog.ComputerName);
  JSON_Object.AddPair('eventCode', aEventLog.EventCode.ToString);
  JSON_Object.AddPair('message', aEventLog.Message);
  JSON_Object.AddPair('recordNumber', aEventLog.RecordNumber.ToString);
  JSON_Object.AddPair('logFile', aEventLog.LogFile);
  JSON_Object.AddPair('eventData', aEventLog.EventData);
  fJSON_Array.Add(JSON_Object);
end;


{ TCSVReaderOutput }

procedure TCSVReaderOutput.AddEventLog(aEventLog: TRBWindowsEvent);
var
  s: string;
  procedure AddValue(aValue: string);
  begin
    s := s + aValue + ',';
  end;

begin
  AddHeader;
  AddValue(aEventLog.Category);
  AddValue(aEventLog.ComputerName);
  AddValue(aEventLog.EventCode.ToString);
  AddValue(aEventLog.Message);
  AddValue(aEventLog.RecordNumber.ToString);
  AddValue(aEventLog.LogFile);
  AddValue(aEventLog.EventData);
  AddLine(s);
end;

procedure TCSVReaderOutput.AddHeader;
begin
  if fCSVString = '' then
  begin
    AddLine('category,computerName,eventCode,message,recordNumber,logFile,eventData,');
  end;
end;

procedure TCSVReaderOutput.AddLine(aLine: string);
begin
  fCSVString := fCSVString + aLine + chr(13) + chr(10);
end;

function TCSVReaderOutput.OutputString: string;
begin
  Result := fCSVString;
end;

procedure TCSVReaderOutput.SaveToFile(aFileName: string);
var
  sl: TStringList;
begin
  sl := TStringList.Create;
  try
    sl.Text := fCSVString;
    sl.SaveToFile(aFileName);
  finally
    sl.Free;
  end;
end;

end.

Here are some examples of how to use the output classes.

To write to the Events Log.

procedure TfrmWindowsEvents.AddLogBtnClick(Sender: TObject);
begin
  fWindowsEvents.Writer.WriteInformationToWindowsEvents(MessageEdt.Text);
end;

To read from the Events Log

procedure TfrmWindowsEvents.StringBtnClick(Sender: TObject);
var
  stringsReader: IRBEventReaderOutput;
begin
  stringsReader := TStringsReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(stringsReader);
  stringsReader.SaveToFile('stringsoutput.txt');
  MemoEvents.Lines.CommaText := stringsReader.OutputString;
end;

procedure TfrmWindowsEvents.JsonBtnClick(Sender: TObject);
var
  jsonReader: IRBEventReaderOutput;
begin
  jsonReader := TJSONReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(jsonReader);
  jsonReader.SaveToFile('jsonoutput.txt');
  MemoEvents.Lines.Text := jsonReader.OutputString;
end;

procedure TfrmWindowsEvents.CsvBtnClick(Sender: TObject);
var
  csvReader: IRBEventReaderOutput;
begin
  csvReader := TCSVReaderOutput.Create;

  fWindowsEvents.Reader.PopulateEvents(csvReader);
  csvReader.SaveToFile('csvoutput.csv');
  MemoEvents.Lines.Text := csvReader.OutputString;
end;

One thing to note is that querying the events can be very slow depending on the amount of logs in the database. You can improve this in the Event Viewer application by selecting Windows Logs > Application and from either the main menu 'Action' or from the right click menu select 'Clear Logs'.


Tuesday, 11 May 2021

FastReports Picture Issue

 I've been using FastReports for a few months to produce PDF reports and most of the time it seems to work OK, but have one major problem with pictures. Here is what is happening

1) Multiple reports that need images on a report, these images are small icons.

2) The images are stored in the database and the dataset brings them back fine. 

3) Previewing the report in the report designer shows the images OK.

4) Exporting the report to PDF, the images no longer appear. I am exporting the report in code not directly from the preview.

If I put a picture control on the report and load an image, this works OK, it is just when I set the 'DataSet' and 'DataField' properties of the picture component they do not appear.

I think the issue might relate to when I activate the Datasets, but it could be something else like a property I am failing to set.


frxReport := TfrxReport.Create(nil);
frxPDF := TfrxPDFExport.Create(nil);
dsList := TObjectList<TfrxDBDataset>.Create();
try
    frxReport.LoadFromFile(fr3file);
    frxReport.DataSets.Clear;
    frxReport.EngineOptions.SilentMode := true;
    frxReport.EngineOptions.EnableThreadSafe := true;
    frxReport.EngineOptions.UseFileCache := False;


    frxPDF.fileName := aPDFFile;
    frxPDF.ShowDialog := False;
    frxPDF.ShowProgress := False;
    frxPDF.OpenAfterExport := False;
    frxPDF.Compressed := true;
    frxPDF.Background := true;

    // Populate datasets - cannot show the code for this
    for spBandDS in dsList do
    begin
        if not(spBandDS.DataSet as TStoredProc).Active then
        begin
            (spBandDS.DataSet as TStoredProc).Open();
            ActivateDataSource(frxReport, spBandDS);
        end;
    end;
    frxReport.PrepareReport(true);
    frxReport.Export(frxPDF);
finally
    for spBandDS in dsList do
    begin
        spBandDS.DataSet.Close;
    end;      
    dsList.Free;
    frxPDF.Free;
    frxReport.Free;
end;

Has anyone else had similar issues with the Picture component and FastReports?

Latest

I've managed to fix the problem by creating a function that does this:

var
  image: TFrxPictureView;
  m: TMemoryStream;
begin
  if (aDataset.Name = aDatasetName) then
  begin
    image := aReport.FindObject(aImageName) as TFrxPictureView;
    if Assigned(image) then
    begin
      image.DataSet := aDataset;
      if image.IsDataField and image.DataSet.IsBlobField(image.DataField) then
      begin
        m := TMemoryStream.Create;
        try
          image.DataSet.AssignBlobTo(image.DataField, m);
          image.LoadPictureFromStream(m);
        finally
          m.Free;
        end;
      end;
    end;
  end;

Then then passing in the parameters of the report, band dataset, name of the dataset and the image component name. Basically at runtime it needs to assign the dataset again.

Friday, 9 October 2020

TWebModule could be a lot better

I've been using TWebModule on a project this year and even though it does what I want it to it is a bit rubbish when there are a lot of actions. It would be nice for it to do the following:

  • Remember the position of the actions form, I always have to move it to select the object inspector.
  • With over one hundred actions in there is would be useful to have a search option, filters (maybe for 'get' and 'post' methods) and sort options in the header. This would make it a lot easier to locate the action in the list.
  • Right click option, or double click to go to the 'OnAction' code.
Not sure if many other Delphi developers use TWebModule or if there is a better alternative. 

Insert Only Database - Pros and Cons

Explanation

In a traditional database updates and deletes are allowed, this destroys data which can sometimes be considered undesirable. In an ‘Insert’ only database or ‘Point in Time’ database only inserts can be performed.


Additional Fields Required


DateCreated – Date and time the record was created.

DateEffective – Date and time the record becomes effective, this can be different than DateCreated for numerous reasons.

DateEnd – Date and time the record is ceased to be effective.

DateReplaced – Date and time the record was replaced by another.

OperatorID or SessionID – User related to the creation of the record.


The date and time fields may also require a UTC offset field. Therefore a total of 9 fields are required.


How are updates done?


This is more complex than a typical update.

  1. Locate the existing record.
  2. Flag it as ‘ended’ and ‘replaced’ with the current date time or the time the update will be applied.
  3. Insert a new record and copy some of the existing field values and the new field values.

Pros and Cons


Pros:

  • Rollback to a point in time is possible.
  • Triggers are not required.
  • All changes are logged, it is not possible for a field to be added that is not included in the audit.
  • Less Locks.
  • Make data changes that do not ‘Go Live’ until a specified date and time.


Cons:

  • Table size is significantly larger if a lot of record changes are required. Additional 9 fields required for every table.
  • Need views on all tables.
  • If replication is a requirement all audit database is also replicated on replication servers.
  • No option to exclude fields from the audit.
  • Making a change (typically an update) is more complex. 
  • Foreign key and relationships can be more complex.