ADFS from a .Net SDK application

Overview 

In a previous post I examined how to configure RM to authenticate via ADFS when using the native client, in this post I show how to write a .Net SDK application to authenticate via ADFS.

The code

This is the console application I used in the video above

class Program
{

    private static ClientAuthenticationMechanism getAuthenticationMechanism(string dbId)
    {
        foreach (int authenticationMethod in new int[] { 3, 2, 0 })
        {
            if (Database.IsAuthenticationMethodSupported("G1", (ClientAuthenticationMechanism)authenticationMethod))
            {
                return (ClientAuthenticationMechanism)authenticationMethod;
            }
        }

        throw new ApplicationException("No Authentication method found.");
    }


    static void Main(string[] args)
    {
        TrimApplication.TrimBinariesLoadPath = "D:\\82\\x64\\Debug";
        TrimApplication.HasUserInterface = true;
        TrimApplication.Initialize();

        using (Database database = new Database())
        {
            database.Id = "G1";
            database.WorkgroupServerName = "local";
            database.AuthenticationMethod = getAuthenticationMechanism("G1");

            database.Connect();


            Console.WriteLine("{0} - {1}", database.CurrentUser.FullFormattedName, database.CurrentUser.Uri);
        }
    }
}

Warning

As I mention in the video the method Database.IsAuthenticationMethodSupported will only return a valid response if the native client has connected previously from the current machine.  If no connection has been made previously then all authentication mechanisms will return true.

Record creation with ACL in the ServiceAPI

I had a question the other day about creating a new record and setting the ACL at the same time.  This led me to realise that the only sample for this in the ServiceAPI documentation was quite complex (and only for record update not creation).  In an attempt to remedy this I have added a new sample.  To use this copy the file into your  ServiceAPI 'examples' folder and then open the URL: http://<myServer>/HPRMServiceAPI/examples/CreateWithACL_JSON.

Code

The actual code simply composes a JSON object to represent the new record and includes an AccessControlList property containing a valid ACL object.  This object is documented in the ServiceAPI documentation.  The JSON that is posted looks like this:

var acl = {
    "FunctionEnum": "RecordAccess",
    "FunctionProfiles": {
        "DestroyRecord": {
            "Setting": "Private",
            "ReferenceStyle": "NoRefNoCopy",
            "AccessLocations": [
                {
                    "FindBy": "me"
                }
            ]
        }
    }
}             

var formData = {
    'RecordTitle': $("#recordForm :input[name=RecordTitle]").val(),
    'RecordRecordType': $("#recordForm :input[name=RecordRecordType]").val(),
    'AccessControlList': acl                                        
}

The jquery to post the JSON looks like this:

$.ajax({
    url: action,
    type: 'POST',
    dataType: 'json',
    contentType: 'application/json',
    data: JSON.stringify(formData)
})

Using the .Net libraries

Creation of a record while setting the ACL is also available via the .Net client libraries.  The code below constructs an TrimAccessControlList object for the purpose of setting DestroyRecord to private.

TrimClient trimClient = new TrimClient("http://localhost/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

TrimAccessControlList acl = new TrimAccessControlList();
acl.FunctionProfiles = new FunctionProfilesDictionary();
acl.FunctionProfiles.Add("DestroyRecord",
    new TrimAccessControlFunction()
    {
        Setting = AccessControlSettings.Private,
        ReferenceStyle = AccessReferenceStyle.NoRefNoCopy,
        AccessLocations = new List<LocationRef>() { 
            new LocationRef() { Uri = 1 }
        }
    });

Record request = new Record()
{
    Title = "from client with ACL",
    RecordType = new RecordTypeRef() { Uri = 2 },
    AccessControlList = acl
};

RecordsResponse response = trimClient.ServiceClient.Post<RecordsResponse>(request);

Java and the ServiceAPI

Intro

On a sunny Friday afternoon my mind turns away from deadlines and release dates and toward pet projects.  My impression has been that we do not have many Java developers in the Records Manager world, at least I don't hear from them.  Maybe that is just because they have given up long ago.  Given the seeming lack of interest we have never put a lot of time into supporting Java developers, the question for me became, how can we do something that is more than nothing without the 'gung ho' of an official toolkit?

Open Source

As of last night our unofficial Codeplex site contains a Java toolkit for consuming the ServiceAPI.  It is not complete missing some non-core bits (such as the ability to fetch messages, object and property definitions etc).  It is also very much a part time project by a part time Java programmer (me).  If you happen to be an expert Java programmer please make some suggestions (or create a pull request), it would be great to make this thing better.

The future

The amount of time I put into this will correlate pretty directly to the interest I get, if no one asks questions (or complains) not much will happen.  

Lookupset Items in the .Net SDK

Overview

In RM 82 Lookup Set Items were freed from the restrictions of being a child object type.  This means that a lookup set item is now a main object, at the same level as the lookup set itself.  Why was this done?  Mainly to allow for large lookup sets.  The parent child relationship among RM objects has practical limits on the number of children a main object can have.

Adding a new lookup item

The new way to add a new item is to instantiate it using the lookup set as the constructor parameter, for example:

LookupSet lookupSet = new LookupSet(database, 9000000000);
LookupItem lookupItem = new LookupItem(lookupSet);

lookupItem.Name = "My Test";
lookupItem.Save();

Fetching lookup items

You can get a list of lookup items just as you would any other main object type (e.g. record or location), using the TrimMainObjectSearch, as seen here.

TrimMainObjectSearch lookupItemSearch = new TrimMainObjectSearch(database, BaseObjectTypes.LookupItem);
lookupItemSearch.SetSearchString("set:9000000000");

foreach (LookupItem item in lookupItemSearch)
{
    Console.WriteLine(item.Name);
}

Instantiating a record using the .Net SDK

Overview

Once you know its URI (or Record Number) there are a few ways to instantiate a record in the .Net SDK, choose the one that best suites your needs.  Each main object type (e.g. Location, History, Hold etc) follows the same pattern seen below.

Constructor

Use the constructor with either the record URI or number.  This method will throw a TrimException if the record is not found.

By URI

try
{
    Record record = new Record(database, 900000000);
    Console.WriteLine(record.Title);
}
catch (TrimException ex)
{
    Console.WriteLine(ex.Message);
}

By record number

try
{
    Record record = new Record(database, "REC_1");
    Console.WriteLine(record.Title);
}
catch (TrimException ex)
{
    Console.WriteLine(ex.Message);
}

Database 'find by' method

This will not throw an exception but the resulting object will be null if the Record is not found.

By URI

Record record = database.FindTrimObjectByUri(BaseObjectTypes.Record, 90000000) as Record;
if (record != null)
{
    Console.WriteLine(record.Title);
}

By record number

Record record = database.FindTrimObjectByName(BaseObjectTypes.Record, "REC_1") as Record;
if (record != null)
{
    Console.WriteLine(record.Title);
}

By URN

Find by URN can be useful for those occasions when you want to persist a unique identifier for a record (or other object type).

TrimMainObject trimMainObject = database.FindTrimObjectByURN("trim:H1/rec/9000000000");
if (trimMainObject != null)
{
    Console.WriteLine(trimMainObject.Name);
}

ADFS HP RM Client authentication

Overview

In HP Records Manager 8.2 we added ADFS authentication as an option in the native windows client.  In this video I run through configuring this in our lab environment.

Things to copy and paste

Here are the various powershell commands I used on my ADFS server.

Create the ADFS client for HPRM.

Add-AdfsClient -Name "HPRM ADFS Client" -ClientId "ab762716-544d-4aeb-a526-687b73838a33" -RedirectUri "urn:ietf:wg:oauth:2.0:oob" -Description "OAuth 2.0 client for HPRM"

Set the token lifetime to force HP RM to check back with ADFS at defined intervals.

Set-AdfsRelyingPartyTrust -TargetName "My Relying Party Trust" -TokenLifetime 10

Tell ADFS to issue refresh tokens to all devices, you may also choose to specify WorkplaceJoinedDevices.

Set-AdfsRelyingPartyTrust -TargetName "My Relying Party Trust" -IssueOAuthRefreshTokensTo AllDevices

In the video I set the refresh token life in the UI but it can also be done with this powershell command. the maximum value ADFs will allow is 9999 minutes.

Set-AdfsProperties -SSOLifetime 480

ADFS client side authentication (part 2)

Demo

This demonstrates the creation of a simple console application which uses ADFS to authenticate and then passes those credentials to the ServiceAPI.  If you are interested in ADAL then read Vittorio Bertocci's blog, particularly this post.  The token cache I use in the below video can be found here, the code I wrote below is here.

Refresh tokens

A refresh token will allow a client to keep their credentials cached for days rather than hours however in my experience ADFS does not issue refresh tokens by default. You may wish to do some research on refresh tokens and decide whether or not you want to support them.  If yes then use the following powershel command to enable them for your relying party trust.  You can choose to issue refresh tokens to AllDevices or WorkplaceJoinedDevices.

Set-AdfsRelyingPartyTrust -TargetName "davidc2012 ServiceAPI" -IssueOAuthRefreshTokensTo AllDevices

ADFS client side authentication (part 1)

Note

In version 8.3 and later it is not required that you update the ServiceAPI itself to support OAuth as it will support it by default.  As long as you send the Bearer token in the Authentication header and set up the <authentication> element in hptrim.config.

Demo

In this video I configure the ServiceAPI  to force it to use my ADFS instance for authentication for client side applications.  This is achieved by using the OWIN framework to enable OAuth2 in the ServiceAPI instance.  I also look briefly at what is required on the ADFS side to make all this work.

The Code

Here are the resources I used in the video:

The command I used to create the ADFS client:

Add-ADFSClient -Name "MySAPIClient" -ClientId "A1CF1107-FF90-4228-93BF-26052DD2C714" -RedirectUri "https://davidc2012.trim.lab/HPRMServiceAPI/"

Database.TrustedUser and web applications

Background

When building a client side SDK application authentication is rarely anissue, you simply connect your database and allow it to connect using the user's credentials.  Server side applications can be a little more complex and require some form of impersonation.

.Net impersonation

In the past when we have built web application or web services we have sometimes relied on .Net impersonation.  This allows us to impersonate the credentials used by the browser (assuming authentication is enabled in IIS but has a few issues, including:

  • challenges when using multiple domains,
  • inability to plug in 3rd party authentication providers, and
  • without careful design a requirement to allow all impersonated users access to the server operating system.

Database.ConnectAs()

The Database object has a method called ConnectAs(), which due to the fact that you must pass the username and password is of limited value.

Database.SpawnImpersonatedDatabase()

To the best of my knowledge this exists only for backwards compatibility, I do not recommend using it.

Database.TrustedUser

This is where you should be if you are creating a server side application, such as a web site or web service.  TrustedUser requires that the credentials used to run the application have permission to impersonate other users, there are two ways for a user account to have this permission, if it is:

  • the same account used to run the workgroup server, or
  • an account registered in 'trusted server accounts' in Enterprise Studio.

For an IIS web application that user that must have this permission is the Identity used by the Application Pool attached to the IIS application.

Once you are running as one of these users then you can impersonate any RM user like this:

using (Database database = new Database())
{
    database.Id = "H1";
    database.WorkgroupServerName = "local";
    database.TrustedUser = "PersonA";
    database.Connect();
}


ServiceAPI - Attach an action

Background

Today's question of the day from the forums is how to attach an action from the ServiceAPI.  Took me a few minutes to work out exactly what was going on here as the ServiceAPI inherits a little bit of the opacity of the SDK.

The Code

TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

Record request = new Record();
request.Uri = 9000000012;

request.ActionsToCall = new List<IMainObjectActionRequest>() { 
    new AttachAction() { 
        ActionToAttach = new ActionDefRef() { Uri = 9000000008 }, 
        NewAssignee = new LocationRef() { Uri=1 }
    }
};

try
{
    var response = trimClient.Post<RecordsResponse>(request);
    Console.WriteLine(response.Results[0].Uri);
}
catch (WebServiceException wex)
{
    Console.WriteLine(wex.ErrorMessage ?? wex.Message);
}

Some comments

RecordActionUri

There is a property called RecordActionUri.  This is not the Uri of the action to attach but a Record Action before (or after) you which this action should start.  The help from the SDK reads: 'Insert a new action into the list of actions, having it start before or after the nominated attached record action.'

ActionToAttach

When specifying the ActionToAttach you must specify the Uri of the action, it will not be found by name.

Paging issues in the web client

Background

Somewhere lost in the mists of time (HPRM 8.1.x or so) there was an issue in the HPRM web client which caused the second page of your search results to sometimes contain results that you had already seen in the first page.  We fixed this in a patch of course.

But wait!

Upgrading to the latest patch is not always achievable, maybe, just maybe there is a work-around? Turns out there is, try putting this inside the routeDefaults element of your hprmServiceAPI.config file:

<add
   name="Record"
   model="Records"
   sortBy="recRegisteredOn- unkUri"
   includePropertyDefs="true"
/>

What  is going on here? The sort by Uri puts the search results in a predictable and static sequence, the sort by date registered puts them in a useful sequence.

Not only a work-around

Given that the Web Client does not yet support custom sorting you may wish to use this technique to sort search results by something more meaningful than the default.

SDK Searching by record type

Today's question of the day, searching by record type...

The imperative approach

Here we use the methods built into the TrimMainObject search to build up the search clauses to search for the record type with Uri 1.

using (Database database = new Database())
{
    database.Id = "H1";
    database.WorkgroupServerName = "local";
    database.Connect();

    TrimMainObjectSearch search 
      = new TrimMainObjectSearch(database, BaseObjectTypes.Record);

    TrimSearchClause typeClause = new TrimSearchClause(database, BaseObjectTypes.Record, SearchClauseIds.RecordType);
    typeClause.SetCriteriaFromObject(new RecordType(database, 1));

    search.AddSearchClause(typeClause);

    foreach (Record record in search)
    {
        Console.WriteLine(record.Title);
    }
}

String searching

I rarely take the approach demonstrated above, in fact I spent 15 minutes trying (and failing) to work up a more complex example.  Instead I use string searching, like this:

using (Database database = new Database())
{
    database.Id = "H1";
    database.WorkgroupServerName = "local";
    database.Connect();

    TrimMainObjectSearch search 
      = new TrimMainObjectSearch(database, BaseObjectTypes.Record);

    search.SetSearchString("recType:1");

    foreach (Record record in search)
    {
        Console.WriteLine(record.Title);
    }
}

Or you can use the square braces to do a sub search, here we search for all records derived from record types with the behaviour of Box or Folder.

using (Database database = new Database())
{
    database.Id = "H1";
    database.WorkgroupServerName = "local";
    database.Connect();


    TrimMainObjectSearch search = new TrimMainObjectSearch(database, BaseObjectTypes.Record);

    search.SetSearchString("type:[behaviour:Box,Folder]");

    foreach (Record record in search)
    {
        Console.WriteLine(record.Title);
    }
}

Composing string searches

If I am stuck composing a string search I usually:

  • open the HPRM client
  • compose my search using the boolean editor, then
  • switch to string mode.

 I can then copy and paste the string search composed for me by HPRM.

A simple COM SDK VB.Net application

In a previous post I demonstrated a simple .Net SDK program, here I will look at a simple COM SDK program.

The code

Here is the code I ended up with in the video.

Sub Main()
    Dim database As New TRIMSDK.Database
    Dim record As TRIMSDK.Record = Nothing
    Dim container As TRIMSDK.Record = Nothing

    Try
        database.Id = "H1"
        database.WorkgroupServerName = "local"
        database.Connect()

        record = database.GetRecord(9000000001)
        container = database.GetRecord("REC_267")

        record.AltContainer = container

        record.Save()
    Finally
        If database IsNot Nothing And database.IsConnected Then
            database.Disconnect()
            System.Runtime.InteropServices.Marshal.ReleaseComObject(database)

            If record IsNot Nothing Then
                System.Runtime.InteropServices.Marshal.ReleaseComObject(record)
            End If

            If container IsNot Nothing Then
                System.Runtime.InteropServices.Marshal.ReleaseComObject(container)
            End If

        End If
    End Try
End Sub

How to create a communication

Background

A common activity around a record is to send and receive communications in relation to it, be they physical letters or emails.  If you want an audit trail of communications sent and received you may be interested in the communication object.

Create a communication

The code below uses the ServiceAPI .Net client library to create a new communication. Some points to note are:

  • a communication must be related to a record, the Record property controls this.
  • a communication must have a sender and recipient and these must be HPRM location objects, if you do not have a location for the recipient you must create one.
  • the ChildDetails is a child list as described in this post.
            TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI");
            trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;
            
            Communication communication = new Communication();
            communication.Record = new RecordRef() { Uri = 9000000018 };
            communication.Direction = CommunicationDirection.Outgoing;     

            communication.ChildDetails = new List<CommunicationDetail>() { 
                new CommunicationDetail() { AddressType = SnapAddressType.Email, 
                    Direction = CommunicatorType.Sender,  
                    Location = new LocationRef() { Uri = 1 }
                },
                new CommunicationDetail() { 
                    AddressType = SnapAddressType.Email, 
                    Direction = CommunicatorType.Recipient, 
                    Location = new LocationRef() { Uri = 9000000001}
                }
            };

            try
            {
                trimClient.Post<CommunicationsResponse>(communication);
            }
            catch (WebServiceException ex)
            {
                Console.WriteLine(ex.ErrorMessage);
            }

JSON

The JSON object posted by the code above looks like this.

{
    "ChildDetails": [
        {
            "TrimType": "CommunicationDetail",
            "CommunicationDetailAddressType": {
                "Value": "Email"
            },
            "CommunicationDetailDirection": {
                "Value": "Sender"
            },
            "CommunicationDetailLocation": {
                "TrimType": "Location",
                "Uri": 1
            },
            "Delete": false,
            "Uri": 0
        },
        {
            "TrimType": "CommunicationDetail",
            "CommunicationDetailAddressType": {
                "Value": "Email"
            },
            "CommunicationDetailDirection": {
                "Value": "Recipient"
            },
            "CommunicationDetailLocation": {
                "TrimType": "Location",
                "Uri": 9000000001
            },
            "Delete": false,
            "Uri": 0
        }
    ],
    "TrimType": "Communication",
    "CommunicationDirection": {
        "Value": "Outgoing"
    },
    "CommunicationRecord": {
        "TrimType": "Record",
        "Uri": 9000000018
    },
    "Uri": 0
}

If you are hand-crafting the JSON you may wish to omit all of the unnecesary object notation and send simple values for objects and enums, like this:

{
    "ChildDetails": [
        {
            "CommunicationDetailAddressType": "Email",
            "CommunicationDetailDirection": "Sender",
            "CommunicationDetailLocation": 9000000001
        },
        {
            "CommunicationDetailAddressType": "Email",
            "CommunicationDetailDirection":"Recipient",
            "CommunicationDetailLocation": 1
        }
    ],
    "CommunicationDirection": "Outgoing",
    "CommunicationRecord": 9000000016,
}

The above JSON must be posted to the communication endpoint (e.g. http://MyServer/HPRMServiceAPI/Communication

Searching

Once you have created some communication objects you can of course find records based on their communications.  For example, all records with:

  • communications to or from me: communication:[detail:[location:me]]
  • communications from me: communication:[detail:[location:me and type:sender]]
  • communications to me: communication:[detail:[location:me and type:recipient]]
  • at least one communicationcommunication:[all]
  • communications in a date range: communication:[dated:1/09/2015 to 1/09/2015]

Today's question - creating record relationships from the ServiceAPI

Background

In HPRM records may have a variety of different relationships, from 'Redaction of' to 'Related to'.  These relationships are managed in both the SDK and ServiceAPI via the child collection construct.  To add a relationship you add a new item to the ChildRelationships collection, to remove a relationship you remove an item from this collection.

C#

This code will create a new relationship using the .Net client for the ServiceAPI.

TrimClient trimClient = new TrimClient("http://MyHost]/ServiceAPI");
trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;

Record record = new Record() { Uri = 9000000001 };
record.ChildRelationships = new List<RecordRelationship>() { 
    new RecordRelationship() { 
        RelatedRecord = new RecordRef() { Uri = 9000000003}, 
        RelationType = RecordRelationshipType.IsRelatedTo
    }};

trimClient.Post<RecordsResponse>(record);

Javascript

Assuming you are using jQuery this code will create a record relationship from Javascript, the complete code can be found here.

$().ready(function () {
    $('#recordRelationship').submit(function () {
        var data = { "Uri": $("input[name=Uri]").val() }

        data["ChildRelationships"] = [{
            "RecordRelationshipRelatedRecord": $("input[name=RelatedUri]").val(),
            "RecordRelationshipRelationType": "IsRelatedTo"                    
        }];

        $.ajax({
            url: "Record",
            type: 'POST',
            contentType: 'application/json',
            data: JSON.stringify(data)

        })
        .done(function (response, statusText) {
            alert("success");
        })
        .fail(function (xhr) {
            var err = eval("(" + xhr.responseText + ")");
            alert(err.ResponseStatus.Message);
        });

        return false;
    });
});

Security caveats for a record

Question of the day - Find all security caveats for a record.

HPRM has an expansive SDK and often when I am asked a question I have to do some elementary research to find the answer, that is one excuse although maybe I am just getting forgetful. So I don't forget the process as well as the answer today in addition to answering the question I will step through my thought process.

The thought process

If I do not immediately know the answer I go through a process a little like this:

  1. Everything (almost) in the SDK is a TrimMainObject, TrimChildObject, or property of one of these.  I start typing in Visual Studio, up pops SecurityCaveat, a TrimMainObject.
  2. So, which mechanism relates it to a Record?
    1. Child list? No there is no relevant child list on the record object.
    2. Is there a search clause?  No, you can search for records by caveat but not caveats by record.
    3. Maybe there is some special mechanism?
  3. Time to browse through the record properties, I spot Record.Security, but that is a string property, close but maybe not quite.
  4. Aha... Record.SecurityProfile, looks interesting. Does it contain a list of caveats? No, it has AddCaveat, RemoveCaveat but, wait in, here is something nearly as good...

The code

This will display a comma separated string containing the security level and names of all caveats.

Record rec = new Record(database, 9000000221);
Console.WriteLine(rec.Security);

We could take this list and find all caveats with the corresponding names.

Record rec = new Record(database, 9000000221);
foreach (string caveatName 
    in rec.Security.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
{
    var caveat = database.FindTrimObjectByName(BaseObjectTypes.SecurityCaveat, caveatName) as SecurityCaveat;
    if (caveat != null)
    {
        Console.WriteLine(caveat.Name);
    }
}

Or better still (depending on how many caveats there are) we could loop through all caveats and find the ones we want.

Record rec = new Record(database, 9000000221);
TrimMainObjectSearch caveatSearch = new TrimMainObjectSearch(database, BaseObjectTypes.SecurityCaveat);
caveatSearch.SelectAll();
foreach (SecurityCaveat caveat in caveatSearch)
{
    if (rec.SecurityProfile.IsCaveatOn(caveat))
    {
        Console.WriteLine(caveat.Name);
    }
}

Offline Records

Spending most of my life in the server side world of web services I rarely pay much attention to offline records in HPRM, other people obviously do given that twice in the past week I have been asked questions about searching for them.

Firstly, it will not work from a web service, offline records require access to the user's local machine.

Now, how do I search from the SDK? Just as I would search for any other object type it turns out.

TrimMainObjectSearch search 
   = new TrimMainObjectSearch(database, BaseObjectTypes.OfflineRecord);
search.SetSearchString("status:Draft");

foreach (OfflineRecord offlineRec in search)
{
   Console.WriteLine(offlineRec.Title);
}

Delete all Records

The setup

Had a support call escalated to me a few days ago, customer wanted a method to delete all records.  Sounds interesting I thought, maybe a bit of an edge case, so I suggested they do this:

TrimMainObjectSearch search 
    = new TrimMainObjectSearch(database, BaseObjectTypes.Record);
search.SelectAll();

foreach (Record record in search)
{
    record.Delete()
}

Push back

Of course the customer pushed back, they wanted a more efficient way to delete all records.  I happened to mention this in our daily team meeting only to be informed by a colleague that we do have a 'delete all' function, in fact two.

TrimMainObjectSearch search 
    = new TrimMainObjectSearch(database, BaseObjectTypes.Record);
Record.BulkDeleteAndDeleteContents(search, true);

TrimMainObjectSearch search 
    = new TrimMainObjectSearch(database, BaseObjectTypes.Record);
Record.BulkDeleteAndUnlinkContents(search, new Location(database, 123), true);

These functions are more efficient than my foreach loop given that they bypass all the object cache logic. Seems I learn something new every day!

Example

This code will delete all records that or not container type records.  It will only delete records if there are no problems with doing so, things that might prevent records being deleted include:

  • one or more matching records being under legal hold,
  • one or more matching records having a client relationship with a record not being deleted.
TrimMainObjectSearch search 
    = new TrimMainObjectSearch(database, BaseObjectTypes.Record);
search.SetSearchString("not type:[behaviour:Folder,Series,Box,PaperFolder]");

Record.BulkDeleteAndUnlinkContents(search, database.CurrentUser, false);

Warning

I don't need to say this but don't just copy and paste the code above, make sure you test your search to ensure you only delete what you want to delete.  Take a backup first of course.

Rationale

So what was the customer trying to achieve?  Turns out the customer was HP and we had been testing HPRM datasets for the new companies.  When the split actually happens they want to clean out all test data, leaving containers only, and start again with production data.