PartialUpdate combined with Delete is intermittent

Refering to my previous topic: AutoCrud: how to combine softdelete and hard delete together in a single service?

Following the advice in mythz’s answers:

I decided to move my code from AutoPopulate to AutoApply, mentioned above, with custom behaviors, and it is working well.

However, I am struggling with my combined soft-delete/delete method. It is updating my audit attributes correctly, as required, for my soft-delete step. However, the hard delete is only hard deleting the database record sometimes, and I cannot determine the reasons.

From two approaches mentioned above, my preference was to use the PartialUpdate, as shown below:

// Perform a hard delete, after updating record with a soft delete
public DeleteResponse Delete(DeleteConfiguration request)
{
    try
    {
        using var db = AutoQuery!.GetDb<Configuration>(base.Request);

        // step 1: soft delete prerequiste 
        var response1 = (DeleteResponse)AutoQuery.PartialUpdate<Configuration>(request, base.Request);
        if (response1.IsErrorResponse() || response1.Count == 0)
        {
            return response1;
        }
        else
        {
            // step 2: continue with requested hard delete
            var response2 = (DeleteResponse)AutoQuery.Delete<Configuration>(request, base.Request);
            return response2;
        }
    }
    catch (Exception ex)
    {
        Log!.Error("Exception opening database connection", ex);

        throw;
    }
}

The API definition for the delete service is:

[Tag("App")]
[ValidateIsAuthenticated]
[ValidateHasRole("MANAGE.DATA")]
[Route("/app/Configuration/{Id}", "DELETE")]
[AutoApply(MyAuditBehavior.MyAuditDelete)]
public class DeleteConfiguration : IDeleteDb<Configuration>, IReturn<DeleteResponse>, IDelete
{
    public Guid Id { get; set; } = Guid.Empty;
}

public class DeleteResponse
{
    public Guid Id { get; set; } = Guid.Empty;
    public int Count { get; set; } = 0;   // added to help trace error
    public ResponseStatus? ResponseStatus { get; set; } = null;
}

When the service does not work, it fails at step 2, when the AutoQuery.Delete call returns no errors/exceptions, but the response2.Count = 0.

The record is still in the database table, with the relevant soft-delete values correctly updated by step 1 and my [AutoApply(MriAuditBehavior.MyAuditDelete)]:

public static class MyAuditBehavior
{
    public const string MyAuditQuery      = "MyAuditQuery";
    public const string MyAuditCreate     = "MyAuditCreate";
    public const string MyAuditUpdate     = "MyAuditUpdate";
    public const string MyAuditPatch      = "MyAuditPatch";
    public const string MyAuditDelete     = "MyAuditDelete";
    public const string MyAuditSoftDelete = "MyAuditSoftDelete"; 
}

public class MyDatabaseAuditBehavior
{

    public static void MyAuditFilter(AutoCrudMetadata meta)
    {
        if (meta.HasAutoApply(MyAuditBehavior.MyAuditCreate))
        {
            AppyMyBehavior(meta, "CreatedDataEventId");
        }

        if (meta.HasAutoApply(MyAuditBehavior.MyAuditUpdate))
        {
            AppyMyBehavior(meta, "UpdatedDataEventId");
        }

        if (meta.HasAutoApply(MyAuditBehavior.MyAuditPatch))
        {
            AppyMyBehavior(meta, "UpdatedDataEventId");
        }

        if (meta.HasAutoApply(MyAuditBehavior.MyAuditSoftDelete))
        {
            AppyMyBehavior(meta, "DeletedDataEventId");
        }

        if (meta.HasAutoApply(MyAuditBehavior.MyAuditDelete))
        {
            AppyMyBehavior(meta, "DeletedDataEventId");
        }
    }

    private static void AppyMyBehavior(AutoCrudMetadata meta, string eventId)
    {
        meta.Add(new AutoPopulateAttribute(nameof(IAudit.DataEventOn))
        {
            Eval = "utcNow"
        });
        meta.Add(new AutoPopulateAttribute(nameof(IAudit.DataEventBy))
        {
            Eval = "userSession.AccountId"
        });
        meta.Add(new AutoPopulateAttribute(nameof(IAudit.DataEventId))
        {
            Eval = eventId
        });
    }

}

I am sure I am doing something wrong, but I do not really know enough yet, so any help appreciated.

I would start by logging the generated SQL that’s executed:

OrmLiteUtils.PrintSql();

Which will write the SQL that OrmLite executes to the console, what SQL is executed (or not executed) should hopefully identify what the issue is.

That showed the reason, which is because the executed SQL is

DELETE
FROM App.Configuration 
WHERE Id='8ef9275e-1442-eb11-b519-482ae316d296'
  AND DataEventId=3 
  AND DataEventOn= '2020-12-19 16:08:22' ---------- PROBLEM LINE
  AND DataEventBy = 'e13dda58-d9fc-422a-b903-c5b0ed2570f7'

The problematic line is the timestamp in DataEventOn, which does not find the record, because the value in the columns is 2020-12-19 16:08:22.7466667

If the problem line is commented out and the SQL manually executed, it works correctly.

I expected only the Id to be used in the AutoQueryDelete, the other columns are not needed from my perspective.

It’s due to trying to exact match on a fuzzy data type then. Everything on the Delete Request DTO is used in the DELTE query which is how you’re able to create Services that delete by any custom criteria (i.e. not just the PK).

That makes complete sense, but, as I show above, I only defined the Id in the DeleteConfiguration class, hence my expectation.

Could it be that the DataEvent attributes are “leaking” in from my
MyAuditBehavior.MyAuditDelete?

Can you confirm that the DELETE SQL that’s written to the console is due to this line?

var response2 = (DeleteResponse)AutoQuery.Delete<Configuration>(request, base.Request);

Yeah looks like the AutoPopulate values are added to Delete Query Params that’s used. The main issue is the DTO reuse, if you could use a different DTO without the AutoPopulate values the query wont have them.

Here is the complete output, where I created a new value, with KeyName=string2, then deleted it.

17:52:56.286 +01:00 [Debug] SQL: INSERT INTO "App"."Configuration" ("AccountId","KeyName","KeyValue","Description","DataEventId","DataEventOn","DataEventBy") OUTPUT INSERTED."Id" VALUES (@AccountId,@KeyName,@KeyValue,@Description,@DataEventId,@DataEventOn,@DataEventBy)
PARAMS: @AccountId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @KeyName=string2, @KeyValue=string2, @Description=string2, @DataEventId=1, @DataEventOn=2020-12-19 16:52:56, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7
[17:52:56 DBG] SQL: INSERT INTO "App"."Configuration" ("AccountId","KeyName","KeyValue","Description","DataEventId","DataEventOn","DataEventBy") OUTPUT INSERTED."Id" VALUES (@AccountId,@KeyName,@KeyValue,@Description,@DataEventId,@DataEventOn,@DataEventBy)
PARAMS: @AccountId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @KeyName=string2, @KeyValue=string2, @Description=string2, @DataEventId=1, @DataEventOn=2020-12-19 16:52:56, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7
17:52:56.308 +01:00 [Debug] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Create, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:52:56, @RowsUpdated=1, @RequestType=CreateConfiguration, @RequestBody={"accountId":"e13dda58d9fc422ab903c5b0ed2570f7","keyName":"string2","keyValue":"string2","description":"string2"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
[17:52:56 DBG] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Create, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:52:56, @RowsUpdated=1, @RequestType=CreateConfiguration, @RequestBody={"accountId":"e13dda58d9fc422ab903c5b0ed2570f7","keyName":"string2","keyValue":"string2","description":"string2"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
17:52:56.322 +01:00 [Debug] SQL: SELECT "Id", "AccountId", "KeyName", "KeyValue", "Description", "DataEventRv", "DataEventId", "DataEventOn", "DataEventBy" FROM "App"."Configuration" WHERE "Id" = @Id
PARAMS: Id=04d8fea3-1a42-eb11-b519-482ae316d296
[17:52:56 DBG] SQL: SELECT "Id", "AccountId", "KeyName", "KeyValue", "Description", "DataEventRv", "DataEventId", "DataEventOn", "DataEventBy" FROM "App"."Configuration" WHERE "Id" = @Id
PARAMS: Id=04d8fea3-1a42-eb11-b519-482ae316d296
17:53:22.566 +01:00 [Debug] CreateRequestAsync/requestParams:
[17:53:22 DBG] CreateRequestAsync/requestParams:
17:53:22.580 +01:00 [Debug] SQL: UPDATE "App"."Configuration" SET "DataEventOn"=@DataEventOn, "DataEventBy"=@DataEventBy, "DataEventId"=@DataEventId WHERE (Id = @0)
PARAMS: @0=04d8fea3-1a42-eb11-b519-482ae316d296, @DataEventOn=2020-12-19 16:53:22, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @DataEventId=3
[17:53:22 DBG] SQL: UPDATE "App"."Configuration" SET "DataEventOn"=@DataEventOn, "DataEventBy"=@DataEventBy, "DataEventId"=@DataEventId WHERE (Id = @0)
PARAMS: @0=04d8fea3-1a42-eb11-b519-482ae316d296, @DataEventOn=2020-12-19 16:53:22, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @DataEventId=3
17:53:22.583 +01:00 [Debug] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Patch, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:53:22, @RowsUpdated=1, @RequestType=DeleteConfiguration, @RequestBody={"id":"04d8fea31a42eb11b519482ae316d296"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
[17:53:22 DBG] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Patch, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:53:22, @RowsUpdated=1, @RequestType=DeleteConfiguration, @RequestBody={"id":"04d8fea31a42eb11b519482ae316d296"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
SQL: DELETE FROM "App"."Configuration" WHERE "Id"=@Id AND "DataEventId"=@DataEventId AND "DataEventOn"=@DataEventOn AND "DataEventBy"=@DataEventBy
PARAMS: @Id=04d8fea3-1a42-eb11-b519-482ae316d296, @DataEventId=3, @DataEventOn=2020-12-19 16:53:22, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7
17:53:38.194 +01:00 [Debug] SQL: DELETE FROM "App"."Configuration" WHERE "Id"=@Id AND "DataEventId"=@DataEventId AND "DataEventOn"=@DataEventOn AND "DataEventBy"=@DataEventBy
PARAMS: @Id=04d8fea3-1a42-eb11-b519-482ae316d296, @DataEventId=3, @DataEventOn=2020-12-19 16:53:22, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7
[17:53:38 DBG] SQL: DELETE FROM "App"."Configuration" WHERE "Id"=@Id AND "DataEventId"=@DataEventId AND "DataEventOn"=@DataEventOn AND "DataEventBy"=@DataEventBy
PARAMS: @Id=04d8fea3-1a42-eb11-b519-482ae316d296, @DataEventId=3, @DataEventOn=2020-12-19 16:53:22, @DataEventBy=e13dda58-d9fc-422a-b903-c5b0ed2570f7
SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Delete, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:53:38, @RowsUpdated=0, @RequestType=DeleteConfiguration, @RequestBody={"id":"04d8fea31a42eb11b519482ae316d296"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
17:53:38.195 +01:00 [Debug] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Delete, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:53:38, @RowsUpdated=0, @RequestType=DeleteConfiguration, @RequestBody={"id":"04d8fea31a42eb11b519482ae316d296"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster
[17:53:38 DBG] SQL: INSERT INTO "Log"."Audit" ("EventType","Model","ModelId","EventDate","RowsUpdated","RequestType","RequestBody","UserAuthId","UserAuthName") VALUES (@EventType,@Model,@ModelId,@EventDate,@RowsUpdated,@RequestType,@RequestBody,@UserAuthId,@UserAuthName)
PARAMS: @EventType=Delete, @Model=Configuration, @ModelId=04d8fea3-1a42-eb11-b519-482ae316d296, @EventDate=2020-12-19 16:53:38, @RowsUpdated=0, @RequestType=DeleteConfiguration, @RequestBody={"id":"04d8fea31a42eb11b519482ae316d296"}, @UserAuthId=e13dda58-d9fc-422a-b903-c5b0ed2570f7, @UserAuthName=max.muster

Unfortunately, my logging statements appear twice, a non-servicestack issue, which I have not yet resolved. For example, the two lines prefixed below have the same content:

17:52:56.322 +01:00 [Debug]
[17:52:56 DBG]

The AutoPopulate values are being used in DELETE queries, I’d expect you can convert it to a different private Delete AutoQuery DTO and use that instead, e.g:

class RealDeleteConfiguration 
    : IDeleteDb<Configuration>, IReturn<DeleteResponse>, IDelete
{
    public Guid Id { get; set; }
}

public DeleteResponse Delete(DeleteConfiguration request)
{
    //...
    // step 2: continue with requested hard delete
    var useForDelete = request.ConvertTo<RealDeleteConfiguration>();
    var response2 = (DeleteResponse)AutoQuery.Delete<Configuration>(useForDelete, Request);
    return response2;
}

Or if it’s only ever a single field, no need for automapping:

var useForDelete = new RealDeleteConfiguration { Id = request.Id };

That worked, in the few tests I managed to do quickly, so I am very happy, thank you very much.

Although, I do not really understand why I need the private RealDeleteConfiguration class. Is it to avoid the AutoApply attributes being used in the matching?

1 Like

Right, the DTO reuse is the issue as DELETE requests use all data for its queries and there’s only a single Metadata configuration maintained per type which is used for all operations.

Fair enough, thank you for the weekend support, makes my life much easier for tomorrow’s work.

1 Like