The complexity of complex return types

Since Business Central 2021 Wave 1 – v18, if you will – we are able to return about ANY type from a procedure. For many languages, that’s the most normal thing, but for Business Central’s AL language, it was not. So, at the time this was announced, many people were ecstatic and talking about it in sessions, on blog posts, on Twitter and so on.

And me as well. In fact, I did a session on the Dutch Dynamics Community, and one for Directions. The one for Directions was recorded and shared on the Directions Community Portal, and you can find the recording here: Webinar Preparing For The Future With Business Central . At 45:19, I start explaining the “Complex Return Types”.

Well .. since you can simply watch the recording, and since it’s much easier to explain it in a video in stead of writing a blog post, I’m going to let you watch the recording for the specifics around what “Complex Return types” are for Business Central.

What I wanted to focus on in this post – and what I didn’t realize at the time I did those sessions – was the reason behind the fact that it seemed to be so hard to really work with records. So .. let me try to explain why it’s behaving the way it does.

The “problem”

Well, consider a function that would return a filtered set of records:

It would make it so easy to get all subrecords from some kind of main record, like in this case:


Thing is – how do you work with it? If I get back a set of records, I most likely would either like to filter some more, and/or loop it, right? Well, I hope you can understand that this absolutely doesn’t make any sense:

Every next would re-get the set of records, so … endless loop it is 😉.

Well, in a way, this would make sense:

Although, when assigning a Rec to a Rec, it does NOT take over the filters. So, it might make sense, but no way it works, it simply would loop ALL records in the sales lines table.

I hear you thinking – no problem, there is a “copyfilters” statement, so let’s do that! Well, no, and this time, the reason is because that method – for some reason that I can’t wrap my head around – needs a “ByRef” variable .. which I don’t have as a return type, obviously:

And before you ask – same for the copy-method:

Last that I could think of is working with the “GetFilters”. Thing is, there isn’t something like “SetFilters”, only “SetFilter” (singular). So not really generic. I can imagine a way to do this with RecRef/FieldRef, like starting some kind of loop like this:

.. but in all honesty, that’s starting to kind of beat the purpose, doesn’t it? I kind of refused to dive into this much further as I couldn’t see it as performant as it should be either.. . If I’m wrong – please I’m all open for feedback!

And let’s be honest, it seems that simply removing the “byref” necessity on the copyfilters-method would solve this problem .. 🤔.

A named return variable, or not a named return variable, that’s the question!

That’s another question we could ask ourselves indeed. And .. does it really matter? Well, I can tell you, it absolutely does matter. It’s something we wouldn’t really think of – at least I wouldn’t – but when thinking about it, it actually does make sense on how it reacts.

What I’m talking about? Well, let’s consider the difference between option 1:

And option 2:

These two functions give a very different outcome. And that’s because the return mechanism is very different in both functions.

In option 1, you actually put the filter on the variable that is being returned.
In option 2, you put it on the local var, but then an assignment of that local var happens on the return-var. So as such, you do a Rec := Rec .. which .. loses your filters. Remember?

The difference in execution can simply be shown by counting the end-result. Like this:

Option 1 will be the expected result: in my case, 1 sales line:

Although option 2 will lose the filter, so it returns a count of all Sales Lines:

So – be careful when you’d apply this to a ModifyAll or DeleteAll or something like that 😜.

This assignment-behaviour obviously applies to all types. Not just records. Though, I can only think of records suffering from immediate consequences .. may be there are other types that need special attention as well, I don’t know.


Complex Return Types are very useful. Coming up with useful patterns though, is somewhat more challenging. This one pattern for sub-records I would love to be accessible .. So I wouldn’t mind Microsoft to “fix” the ByRef need for CopyFilters.
Or if I’m missing something, I wouldn’t mind to understand what and why ;-). All feedback is always welcome ;-). I at least hope it kind of explains a bit to you as it did to me ;-).

5.00 avg. rating (98% score) - 4 votes

Permanent link to this article:


7 pings

Skip to comment form

    • Dennis Reinecke on July 6, 2021 at 7:49 am
    • Reply

    i also played arond with this few weeks ago..
    if your return-record is temporary you will have much more fun:

    procedure MyProcedure() MyRec: Record Integer temporary;
    i: Integer;
    for i := i to 10 do begin
    MyRec.Number := i;
    procedure MyTest()
    MyRec: Record Integer temporary;
    MyRec := MyProcedure();
    Message(Format(MyRec.Count())); // will be 0

    so you will also need the pass by reference in future
    what is not bad, but you need to know it.

    related to your filter implementation – before you start to split and parse the filter one-by-one. i would suggest to using something like

    procedure MyFunction()
    SalesLine: Record “Sales Line”;

    procedure GetSalesLineView() : Text
    SalesLine: Record “Sales Line”;
    SalesLine.SetRange(“Document Type”, “Document Type”);
    SalesLine.SetRange(“Document No.”, “No.”);

      • waldo on July 6, 2021 at 8:05 am

      Yep – as I said to Marknitek, I didn’t think of that. I’m going to write a separate post soon for completeness … thanks for the comment!

  1. I would use setview/getview instead of copyfilters. Its generic and includes keys. But its still overly complicated. And this was already possible before since we could return text. But ok having the record gives more possibilties.
    To realy be usefull in the looping scenario, records should support the foreach statement then the return type could be used. But then again reverse looping or anything else then standard loops one would still have to use a local var.

      • waldo on July 6, 2021 at 8:03 am

      I didn’t think about using the Get/SetView, and now hate myself for it – but indeed, thanks for the tip, that works and I guess it’s the best we can do for now.

      Indeed, I would also like the foreach-implementation.

      And .. still .. that “byref” in the copyfilters, in all honesty, that still doesn’t make sense to me ;-).

    • Kevin on July 6, 2021 at 8:36 am
    • Reply

    My main reason to want to use a record return type is to be able to program more reliable. In old Navision, it would have been tidy to program functions returning data for description fields with a code length of 30, and we all know they switched to 50 now.. maybe a stupid example as it’s only one field, but I guess my point is clear.

      • waldo on July 6, 2021 at 8:41 am

      Totally clear – and makes total sense!

    • Todd Scott on July 6, 2021 at 8:41 am
    • Reply

    @marknitek beat me to the Get/Set View 😛

    One of the primary reasons I have to pass records to functions byref (var) is to have the filters available. I have always considered this very dangerous and end up using a local copy of the record to make sure I don’t change the passed in record.

    If the returned record had the filters (view) it would be so much more useful.
    SalesLine := SalesHeader.FilterSalesLine();

    procedure FilterSalesLine() SL: Record “Sales Line”
    SL.SetRange(“Document No.”, “No.”);
    SL.SetRange(“Document Type”, “Document Type”);

    Makes more sense and seems safer than

    procedure FilterSalesLine(var SL: Record “Sales Line”)
    SL.SetRange(“Document No.”, “No.”);
    SL.SetRange(“Document Type”, “Document Type”);

    • Matt Keyes on July 6, 2021 at 1:47 pm
    • Reply

    I remember seeing this named variable vs. not named variable when this feature of AL was released. It doesn’t make sense to me that you would design functionality around whether or not a variable has a name, though. That seems almost like a side effect or bug than it does an actual feature

    • Mikkel Vilhelmsen on July 10, 2021 at 9:33 am
    • Reply

    Just noticed that MS has officially documented this as a feature rather than a bug 🙁

    It’s a shame, can’t really see what downsides making Exit() return by reference on record types would have had – Oh well, one more footgun in AL.

    • Kamil Sacek on July 14, 2021 at 7:27 am
    • Reply

    The reason why the Copy/CopyFilter needs byRef param is to have filters passed with the record, not only the record data… 😉 By value you are passing only the record values (fields), nothing more. With ref you are passing whole object including inside state (filters etc.) if I am not wrong…

  1. […] for my previous post: The complexity of complex return types. (I could have just updated the post, but since that wouldn’t trigger the people that already […]

  2. […] for my previous post: The complexity of complex return types. (I could have just updated the post, but since that wouldn’t trigger the people that already […]

  3. […] The complexity of complex return types […]

  4. […] you remember my post about The complexity of complex return types? And the update The complexity of complex return types (updated) – Looping a record return […]

  5. […] you remember my post about The complexity of complex return types? And the update The complexity of complex return types (updated) – Looping a record return […]

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: