Database connection caching

Previously if you wanted to avoid the overhead of Database.Connect() in a web service application you had to write some sort of connection pool. In 9.2 this is no longer required as the SDK itself caches connections.  The requirements are that:

  • the previous connection must be Disposed,
  • the subsequent connection must have the same Database Id, work group server name  and trusted user.

Sample Code

private static Database getDatabase(string user = null)
{
    Database database = new Database();
    database.WorkgroupServerName = "local";
    database.Id = "L1";
    if (user != null)
    {
        database.TrustedUser = user;
    }
    _watch.Reset();
    _watch.Start();
    database.Connect();
    _watch.Stop();

    Console.WriteLine(_watch.ElapsedMilliseconds);

    return database;
}


private static void connectDatabase()
{
    string trustedUser = "itu_tadmin";
    using (var db = getDatabase(trustedUser))
    {
    }

    using (var db2 = getDatabase(trustedUser))
    {
    }

    using (var db2 = getDatabase(trustedUser))
    {
    }
}



static void Main(string[] args)
{
    TrimApplication.TrimBinariesLoadPath = @"Your bin folder";
    TrimApplication.Initialize();
    TrimApplication.SetAsWebService("c:\\junk");

    connectDatabase();

}

Demonstration

Event Filtering in 92

Event processor add-ins are great for responding to events on the workgroup server but and now we can optimise them to only respond to the events (and object types) that we are interested in.  Use the 'Configure Events' button in the  'Custom Processes' tab in Enterprise Studio, or watch this video for more information.  For more details on the event processor addins see my previous post and the sample.

Where to store config, web.config or not...

For those who customise WebDrawer on one machine and then push it to a production machine this may prove handy.  I wanted to change the setup of IIS on my dev machine to not update the web.config when I change the authentication settings.  Here is what I did...

1. Go to Feature delegation in IIS Manager

feature_delegation_icon.PNG

2. Select Custom Sites

custom_sites.PNG

3. Set the authentication settings to Read Only

read_only.PNG

4. Clean modified web.configs

Remove the <security /> section from all web.config files that have already been updated, otherwise you will get an error when you attempt to open these applications.  Also check c:\inetpub\wwwroot\web.config to ensure that authentication has not been set here.

5. Update applications in IIS

Go through each application in IIS and ensure the authentication settings are correct, this will update applicationHost.config.

Summary

Setting these settings to Read Only prevents them being updated in the application specific web.config, instead they will be set in the applicationHost.config

Email Link - Delete Check in Style

The CM Rambler showed how to improve the Email Link Admin console, this post shows how you might add a facility to delete a user's Check in Style from within the admin console.  My main motivation for this is that there is a version of Content Manager out there which makes it difficult for a user to delete a user's check in style from the client, which is a problem when, for example, a user leaves the organisation.

 

The code

As per the video above

An empty TH

<th></th>

A table cell containing a button

<td><button class="btn btn-small" data-place-uri="@record.Uri">Delete</button></td> 

The script

$("button[data-place-uri!=''][data-place-uri]").on("click", function (event) {
    var placeUri = $(this).attr('data-place-uri');

    $.ajax({
        url: "CheckinPlace?q=Uri:" + placeUri + "&resultsOnly=true",
        data: {
            properties: "CheckinPlaceCheckinAs"
        },
        dataType: 'json'
    })
    .done(function (data) {

        for (var n = 0; n < data.Results.length; n++) {
            var place = data.Results[n];

            if (place.CheckinPlaceCheckinAs) {
                $.ajax({
                    url: "CheckinStyle/" + place.CheckinPlaceCheckinAs.Uri + "/Delete",
                    type: 'POST',
                    dataType: 'json',
                    contentType: 'application/json'
                })
                .done(function (response, statusText) {
                    if (statusText === "success") {
                        window.location.reload();
                    }
                })
            }
        }

    });

});

Upload a file and create a new revision

I just added a new sample to the documentation that ships with the ServiceAPI.  In the mean-time here it is, the two step process to first upload a file and then use it to create a new Rendition on a new Record.

You could also use Record.FilePath to use the uploaded file to create the electronic document for the Record itself.

FileInfo fi = new FileInfo("c:\\junk\\test.pdf");

var fileResponse = client.UploadFile(fi, System.Web.MimeMapping.GetMimeMapping(fi.Name));

Record rec = new Record();
rec.RecordType = new RecordTypeRef() { Uri = 2 };
rec.Title = "test";

rec.ChildRenditions = new List();

rec.ChildRenditions.Add(new RecordRendition()
{
    TypeOfRendition = RenditionType.Longevity,
    FromFileName = fileResponse.FilePath,
    Description = "my PDF rendition"
});

FileInfo mainFile = new FileInfo("c:\\junk\\test.docx");

var response = client.PostFileWithRequest(mainFile, rec);

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

Custom Authentication

A couple of years I wrote a post describing Google authentication in the ServiceAPI.  This is an update of that.  The details are in the samples repo and here is a video of me applying the sample from the repo to a WebDrawer and Web Client instance.

Other authentication providers

Remember the repo contains a link the ServiceStack documentation which provides assistance for using (and creating) other authentication providers.

String Search parsing and filtering

Sometimes I overlook some pretty important things.  I realised recently that I had missed an interesting behaviour of the TrimMainObjectSearch string searching.

A valid search with filtering

The search below will search for all records where the Additional Field 'Alcohol Level' is greater than zero and filter on Record Type == "Infringement".

TrimMainObjectSearch recordSearch = new TrimMainObjectSearch(db, BaseObjectTypes.Record);
recordSearch.SetSearchString("AlcoholLevel>0");
recordSearch.SetFilterString("recType:Infringement");

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

An invalid search without filtering

This search is invalid because 'Alcohol Level' is a number field, so no results are returned, everything is good so far.

TrimMainObjectSearch recordSearch = new TrimMainObjectSearch(db, BaseObjectTypes.Record);
recordSearch.SetSearchString("AlcoholLevel>abc");

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

An invalid search with filtering

You might expect that the search below would behave just as the search above and return no results, given the invalid search string, this is not the case.  The invalid search string is discarded and the filter is applied, the result is all Records where Record Type == 'Infringement'. 

TrimMainObjectSearch recordSearch = new TrimMainObjectSearch(db, BaseObjectTypes.Record);
recordSearch.SetSearchString("AlcoholLevel>abc");
recordSearch.SetFilterString("recType:Infringement");

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

The solution

To avoid this problem always check the return value of SetSearchString().

TrimMainObjectSearch recordSearch = new TrimMainObjectSearch(db, BaseObjectTypes.Record);
TrimParserException parserException = recordSearch.SetSearchString("AlcoholLevel>abc");

if (parserException.Bad)
{
    Console.WriteLine(parserException.Message);
}
else
{
    recordSearch.SetFilterString("recType:Infringement");

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

Filter WebDrawer Search Forms

Lets say you have designed multiple search forms which are used via WebDrawer.  These forms provide different fields with which to search.  In addition to this you may also want to filter the results for each form to make them appropriate for the target audience.

Embedding the filter

One approach is to embed a filter based on the search form name. I did this by:

  1. editing Views\Search.cshhtml
  2. enable 'Name' retrieval for the search form
  3. add a filter field based on the name.

Name retrieval

Near the top of Search.cshtml you will see code that looks like this

searchForm = TrimHelper.Search<SearchForm>(
    BaseObjectTypes.SearchForm,
    "all",
    properties: new string[] { "ObjectType", "DefinitionForm" }).FirstOrDefault();

Add the property 'Name' in the propert list, so that it now looks like this:

searchForm = TrimHelper.Search<SearchForm>(
    BaseObjectTypes.SearchForm,
    sfName,
    properties: new string[] { "ObjectType", "DefinitionForm", "Name" }).FirstOrDefault();<p>Hello, World!

Add the filter

Further down the page is found the search form itself, starting with this HTML:

<form id="search-form-form" class="form-horizontal search-form" action="FormSearch" method="GET">

After this line add some code to insert a filter INPUT element, potentially different for each search form.  The code below add a filter for the search form named 'Test Form'

@if (searchForm.Name == "Test Form")
{
    <input type="hidden" name="filter" value="recType:Document" />
}

Warning

This is a convenience, not a security measure.  For example a user could still modify the URL resulting from this search to avoid the filtering.

WebDrawer - Show contained Records

How would we show a list of all contained Records within the details page of a container in WebDrawer?  I can think of at least two ways.

Embed contained Records

Edit detailsView.cshtml and include the code below somewhere near (or at) the bottom.  This will embed up to 500 contained Records in this page.

@if (Model.TrimModel.TrimType == BaseObjectTypes.Record)
{
    Record record = trimObject as Record;
    if (record.IsContainer)
    {
        <h2>Contained Records</h2>
        <table class="table">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Number</th>
                </tr>
            </thead>
            <tbody>
                @foreach(Record containedRec in this.TrimHelper.Search<Record>(
                    BaseObjectTypes.Record,
                    string.Format("recContainer:{0}", record.Uri),
                    properties: new string[] { "RecordTitle", "RecordNumber" },
                    pageSize: 500))
                {
                    <tr>
                        <td><a href="~/Record/@containedRec.Uri">@containedRec.Title</a></td>
                        <td>@containedRec.Number</td>
                    </tr>
                }
            </tbody>
        </table>
    }
}

Embed contained Records within an IFrame

It is also possible to embed the contained Records as an iframe.  The advantage of this is that you can page through a large result set inside the iframe, the cost is that you will need to set up  clean view without the menu and banner and link that up using the routeDefaults (in hprtrim.config).

I can provide examples of the clean view and routeDefaults on request.

<h2>Contained Records</h2>
<iframe src="~/Record?q=@string.Format("recContainer:{0}", record.Uri)" width="100%" height="200"></iframe>

The result

The result of the first option above will look something like this.

Be careful with search grammar items in string searches

I was caught out the other day using hard-coded string searches in a ServiceAPI application.  It can be convenient (or necessary) to store canned searches (e.g. 'extension:docx OR extension:doc') at times.  The thing to remember is that search strings can be localised and your end user may not be using English.

First, the clauses

Typically the English versions of the search clauses should work irrespective of the user's language, so even if the end user has selected French ' extension' should still be OK.  Due to my over cautious nature I still use the internal name (e.g. 'recExtension) which can be found in the list of search clauses in the ServiceAPI.

Then the grammar items

Search grammar items (such as 'or' and 'and') are not language neutral so 'recExtension:doc or recExtensiom:docx' will not produce any results if your end user has selected Dutch as their language, what you will need is 'recExtension:doc of recExtensiom:docx'.

SDK

Getting the caption for a search grammar item in the SDK is simple:

EnumItem item = new EnumItem(AllEnumerations.SearchGrammarItem, (int)SearchGrammarItem.And).Caption

ServiceAPI - .Net

It is nearly as simple to get the grammar items using the ServiceAPI .Net client.

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

var response = client.Get<EnumItemDetailsResponse>(new EnumItemDetails() { Enums = new AllEnumerations[] { AllEnumerations.SearchGrammarItem } });
Console.WriteLine(response.EnumItems[AllEnumerations.SearchGrammarItem].Where(ei => ei.Name == "Not").First().Caption);

ServiceAPI

Or query the ServiceAPI directly:

http://localhost/ServiceAPI/EnumItem?Enums=SearchGrammarItem&format=json

In short

Never hard code string searches using things like 'not', 'or', 'and', 'me' or anything else in the SearchGrammarItems enum.  If you do and someone switches languages then everything is broken.

Using Postman to create a record

Previously I experiment with searching using Postman  Today lets create a record and upload a file in the same request.

Some Code

Here is the HTTP I generated using Postman.

POST /ServiceAPI81/record HTTP/1.1
Host: desktop-39dgcn3
Authorization: Basic aXR1X3RhZG1pbjpUcmltQEhQMQ==
Accept: application/json
Cache-Control: no-cache
Postman-Token: b9575896-9e99-ccf0-4803-452e4fba11d4
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="RecordRecordType"

Document
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="Files"; filename=""
Content-Type: 


------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="RecordAuthor"

david
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="AlcoholLevel"

5
------WebKitFormBoundary7MA4YWxkTrZu0gW--

ServiceAPI impersonation

The configuration section of the ServiceAPI help documents a handy little property called 'trustedToImpersonate'.  This allows your ServiceAPI to choose to trust one or more calling services.

Scenario

This can be useful in a server to server scenario where passing the actual user credentials to the ServiceAPI is not practical.

Configuration

To allow a particular account to impersonate others users set their name in the hptrim.config file as seen below.  TrustedToImpersonate is a regular expression so you can list multiple accounts if you need to.

<hptrim
  serviceFeatures="Razor,Html,Json,Xml,PredefinedRoutes" 
  trustedToImpersonate="trim\\davidc"
  ...
</hptrim>       

Usage

To impersonate someone using the .Net client librarys use the SetUserToImpersonate() method.

TrimClient client = new TrimClient("http://MyServer/ServiceAPI");
client.Credentials = new NetworkCredential("davidc", "my password", "trim");
client.SetUserToImpersonate("someone\\else");

To impersonate from a different context simply send an HTTP header named 'userToImpersonate' with the value being the user's name along with each request.

Warning when setting the ACL via the SDK

Overview

In the native (or web) client if I attempt to set an invalid ACL on a Record I will get a warning, something like this:

So, I asked myself, why was I not getting the same warning from the SDK?

A riddle

When will the appropriate warning appear in rec.ErrorMessage below...

Record rec = new Record(db, "REC_2");

TrimAccessControlList acl = rec.AccessControlList;
acl.SetAllInherited();
Console.WriteLine("A {0}", rec.ErrorMessage);
rec.AccessControlList = acl;
Console.WriteLine("B {0}", rec.ErrorMessage);
rec.Verify(true);
Console.WriteLine("C {0}", rec.ErrorMessage);
rec.Save();
Console.WriteLine("D {0}", rec.ErrorMessage);

The only point at which a warning message will be present in rec.ErrorMessage is at 'B' above.  Due to the vagaries of the SDK property handling (as described here) calling Verify (or Save() clears out the Error and ErrorMessage properties on the Record object.

The motto of the story

If you are interested in getting the warning from setting the ACL then check for it as soon as you set the AccessControlList property.

ServiceAPI searching, GET or POST

It had been in the back of my mind that using HTTP GET for searching in a project I was working on would lead to trouble and several hours of re-factoring later I am proved correct.

Background

The default way we do searching in the ServiceAPI is using HTTP GET, for example:

http://localhost/ServiceAPI/Record?pageSize=20&q=all&filter=extension:docx,xlsx&properties=RecordTitle&format=json

There are several advantages to use the GET verb but there is also a trap which is that IIS has limitations on the length of a URL.  Usually this is not a problem and the ServiceAPI has design features to help keep URLs concise (such as custom property sets) however I encountered a scenario where I could not avoid the IIS error resulting from an overlong URL.

The scenario

The scenario that caught me was needing the flexibility for the end user to specify as many properties in the properties query parameter as they desired and I did not want to use the 'all' property set as I did not want the cost of fetching every record property.  If the user chose too many properties their request would fail.

The solution

The solution is that the ServiceAPI allows you to POST the same request that you usually GET, you just have to post it to a different end point.  There are a couple of examples of this using an form post and AJAX in the documentation, here is an example using the .Net client.

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


PostSearch request = new PostSearch();
request.TrimType = BaseObjectTypes.Record;
request.q = "all";
request.Properties = new PropertyList(PropertyIds.RecordTitle, PropertyIds.RecordNumber);

var response = client.ServiceClient.Send<RecordsResponse>("POST" , "Search", request);

foreach (Record rec in response.Results)
{
    Console.WriteLine(rec.Title);
}