Web Services Black Belt: consuming NAV web services using pure C/AL

MP900406779[1]Have you ever needed to connect to the Web services of one NAV instance from another one? If so, I bet that the approach was something like this: you created a .NET class where you defined a Web or Service reference to the target instance, and then you consumed that .NET class using .NET Framework interoperability. It was kind of clumsy, inflexible, but it worked.

How cool would it be if you could do something like this:

WITH WebService DO BEGIN
  CONNECT(‘http://localhost:7047/DynamicsNAV70/WS/CRONUS%20International%20Ltd/Page/Customer’);

  INIT;
  SETVALUE(‘Name’,’Test Customer’);
  SETVALUE(‘Blocked’,Cust.Blocked::Ship);
  SETVALUE(‘Credit_Limit_LCY’,10000);
  CREATE;

  MESSAGE(‘I just created Customer No. %1 in another NAV instance.’,GETVALUE(‘No’));
END;

As a matter of fact, you can write something like that. You can write exactly that. And it compiles, runs, and accomplishes exactly what you expect it to do. The most beautiful thing, you don’t need to write a single line of code in Visual Studio, or deploy any external dependencies – it uses pure C/AL, and works equally well in NAV 2009 and NAV 2013.

The only thing you need is a simple codeunit that you can download from Mibuso. If you missed the link in the previous sentence, then click here.

I wrote that codeunit as a part of the demo I presented last Wednesday at Mibuso NAV TechDays 2012 in Antwerp, and as promised – I am making the code available for you to use.

This simple codeunit does no magic, it simply harnesses the power of the features built into the .NET Framework. It builds the proxy class and compiles it on the fly, and then uses reflection to instantiate objects, set properties, and call methods to allow you to interact with any NAV page web service.

Before you can consume a NAV page Web service from C/AL you do not need to know anything about the service, except for its URL. If it’s a page web service, you can use it to read, create, update, and delete data in another NAV instance, simply using C/AL.

At this stage, it supports the following page web service functions:

  • Read
  • ReadMultiple
  • Create
  • CreateMultiple
  • Update
  • UpdateMultiple
  • Delete

Right now, I am providing no documentation for it, but I believe it should not be difficult to figure out what it can do by following these couple of examples.

Creating a customer

That’s the example above. Just declare a variable named WebService of type Codeunit 50113, and you are good to go.

Iterating through a set of customers read, with a filter applied

WITH WebService DO BEGIN
  CONNECT(‘http://localhost:7047/DynamicsNAV70/WS/CRONUS%20International%20Ltd/Page/Customer’);
  SETFILTER(‘Balance_LCY’,’>0′);
  SETFILTER(‘Name’,’A*’);
  IF READMULTIPLE THEN
    REPEAT
      MESSAGE(‘Customer %1 %2 has balance of %3’,GETVALUE(‘No’),GETVALUE(‘Name’),GETVALUE(‘Balance_LCY’));
    UNTIL NEXT = 0;
END;
EXIT;

Updating an item

WITH WebService DO BEGIN
  INIT;
  SETVALUE(‘No’,’1000’);
  READ;
  SETVALUE(‘Description’,’Bicycle 2’);
  UPDATE;
END;

Creating a purchase order from a sales order

WITH WebService DO BEGIN
  CONNECT(‘http://localhost:7047/DynamicsNAV70/WS/CRONUS%20International%20Ltd/Page/PurchOrder’);
  INIT;
  SETVALUE(‘Buy_from_Vendor_No’,Rec."Sell-to Customer No.");
  SalesLine.SETRANGE("Document Type",Rec."Document Type");
  SalesLine.SETRANGE("Document No.",Rec."No.");
  IF SalesLine.FINDSET THEN
    REPEAT
      NEWLINE;
      SETLINEVALUE(‘Type’,FORMAT(SalesLine.Type));
      SETLINEVALUE(‘No’,FORMAT(SalesLine."No."));
      SETLINEVALUE(‘Quantity’,SalesLine.Quantity);
    UNTIL SalesLine.NEXT = 0;
  CREATE;
  MESSAGE(‘Purchase Order No. %1 is created in the vendor”s system.’,GETVALUE(‘No’));
END;

I’ll be updating the functionality of this codeunit and uploading a more comprehensive version, as I find time to do this.

This post is a series of posts where I’ll present most of the stuff I talked about at NAV TechDays 2012, and expand those topics into the areas that I either had to leave out due to time constraints, or that I thought were not as interesting as those that I chose to put into the presentation.

Please, let me know how you like this small gadget.

108 thoughts on “Web Services Black Belt: consuming NAV web services using pure C/AL”

    1. @Tarek – I’ve uploaded it on Mibuso, and as soon as they approve the upload, I’ll link it from the blog post. You won’t miss it. Regarding the Visual Studio project, I’ll make a couple of blog posts around that, too, so stay tuned.

        1. Well, if you can do it with page web service proxy class in C#, you can do it here. I don’t think I specifically implemented support for assigning values to lines, but it shouldn’t be a difficult thing to do.

      1. Hi Vjekoslav,
        I have a compile error in the CU, The system canot find table “WebServicesSetup”? is this is a new table? should I create it ? or how can I solve this issue?

        Thanks alot,
        Zayed

        1. You need to create this table. It contains credentials to connect, and from code you have it’s obvious what this table should look like. What it achieves is that it stores the credentials. Just figure your own way how to store and pass credentials to the web service and you are good to go.

  1. You started obviously developing from c# to NAV? Doesn’t matter of course and your way of thinking is giving me new ideas how to look at NAV. Excellent.

    Didn’t see a VS project. More ideas?

    1. Erik: at first I did indeed develop this thing in C#, and then moved it to C/AL, but now it is pure C/AL and no C# at all. There is no VS project. Which VS project are you referring to?

  2. Tarek is mentioning this “…and the source code of the Visual Studio project..”.

    Would you mind sharing the C# version. I love this reflection. You don’t need to document it, just the functions is enough so I can start the debugger to understand it.

  3. Hi,

    Is this Codeunit the (fob and txt) only compatible with NAV2013.

    As I cant import it into a NAV2009 R2 Database.

  4. I just imported the NAV 2009 code, but when I call it an error message “The DotNet variable has not been instantiated.” pops up. I traced it back to the line:
    ——
    AssemblyReferences := AssemblyReferences.CreateInstance(DotNetType.GetType(‘System.String’),2);
    ——
    Any suggestions how to fix this?

  5. Hi Fred

    just want to confirm Vjekoslav solution.
    Thanks Vjekoslav for the solution.

    i had problems using it in NAV 2009 R2 (Build 32012).
    With NAV 2009 R2 Build 33053 the example of Vjekoslav works fine.

  6. Hi Fred,

    Thanks for your great work, however, here is an error due to missing of the Table123456701 at the last function – Authenticate in the object.

    Would you be so kind to release the table for us?
    Thank you very much!

    1. @Roy: Who’s Fred? My name is Vjeko, nice to meet you 🙂 I can’t check this thing about Table 123456701, but I am pretty sure it’s a rogue variable declaration, and that you can safely skip it, but I’ll do a check on my earliest.

      1. Hi.
        A table that is referencend and used in function Authenticate is missing. Variable name is WebServicesSetup and it refers to table 123456701. One could easily remove the variable and insert the values for user name and pwd hard coded, but just to let you know:

        WITH WebServicesSetup DO BEGIN
        IF GET THEN
        ServiceInstance.Credentials := Credential.NetworkCredential(“User Name”,Password)

        /Pauli

        1. @Pauli: thanks for the comment. Yes – I am aware of that, but it can be easily figured out from the code what fields this table should contain. Or – you can do a quick fix that you did.

  7. Hi Fred,

    Another question is, is your solution compatible with https ?
    i just try to use it for calling web service with SSL, but it failed.

    Thanks!

    1. Hi Vjeko,

      I also tried to use this solution with SSL but it failed.
      Do you know how to connect to web service with SSL?

      Thanks in advance.

  8. I’ve used this and it works very well for NAV 2009. Thanks for putting it together! It has some issues with NAV 2013 relating to the XML-consumption objects, though. Someday, maybe I’ll try and figure out how to do it in 2013 and do a write-up on it.

    1. Strange, it should work better with 2013, because it provides better interoperability with .NET. I’ll really have to take a look and figure it out, but if you can also be more specific as to what doesn’t work well, it would help a lot.

  9. Very nice work! I was blown away when you showed us at NavTechDays2012. The compile in memory could be used whenever a need for a new component is required.

  10. hi Vjeko,
    excellent and ingenious tool, we are using it and it works fine on NAV 2009 & 2013.
    Great!

    Some questions please:
    – would be possible in addition to the pages, you can also invoke codeunits published as Web Services?
    for now, Iiput a function in the page that in the “Onload of the page” trigger, lanches the codeunit.

    – it would be possible to extend the existing functionalities, inserting calls (Invokes) in codeunit and xmlports?

    Thanks for support
    Roberto

    1. Roberto – it could be possible, yes. Codeunits for certain, XMLports would also be possible bu more complicated and less intuitive to work with from C/AL. I’ll see if I have time to continue working on this tool.

  11. “It helped” 🙂
    It helped a lot in fact! Thank you for making this awesome tool available to all of us. I was just about to ask the same question as Roberto regarding codeunits, but I see that you have already answered that one. Hope you find time to extend the functionality.
    Thanks again
    Martin

  12. Great stuff works like a charm. UseDefaultCredentials didn’t work for me while being on a local server. So i used:

    //WITH WebServicesSetup DO BEGIN
    // IF GET THEN
    ServiceInstance.Credentials := Credential.NetworkCredential(‘john’,’1234′);
    // ELSE
    // ServiceInstance.UseDefaultCredentials := TRUE;
    //END;

    1. @vanAnaarB: You could have simply supplied username and password into the Web Service Setup table. But still, I am glad the code helped! Good luck with it!

  13. hey i want to delete Sales Order and line from Navision 2013 using web services in C#.net,
    how will i use delete according to my filter
    plz tell me

    Thanks in advance

    1. @Karan: I don’t quite understand what exactly you want to achieve? If you want to delete Sales Order AND line – then it is enough to delete the Sales Order, and all its lines are deleted. If you want to delete a single line for a sales order, then you must call the Delete_SalesLines method on the web service. You must pass the Key parameter to it, and you can read the key from each of the lines in the Sales Order. You can find these in the SalesLines property of the sales order entity.

  14. Hello,
    I have a small problem with this code. I described it here
    http://mibuso.com/forum/viewtopic.php?f=7&t=55040

    But I need to run it on Windows Server 2012 R2. I checked this code on Windows Server 2008 and it works fine. The version of .NET Framework is the same. I’ve heard that in 2012 R2 handling of XML is changed. Maybe this is the cause of the problem?
    Could you help me with this?
    I would be very thankful.

    Thanks in advance

    1. @Damian: can you please let me know which page web service are you trying to access, when you get this parameter count mismatch error? It should have nothing to do with the handling of XML. Also, if you can provide the whole WSDL document of that page, as well as the page object exported as a text file, it would help. I suspect that this is a Worksheet page, or some other page where you have added some field controls above the topmost group control.

        1. @Damian: Well, just as I suspected – the metadata in the page is not correct. For web services to work correctly, you must put all the field controls into a Group control (a FastTab). If you just dump them into the ContentArea, then web services sees each of the fields as a parameter of all service methods for the web service, and my generic web service management code cannot work with that. So – to fix this, just add a Group just below the ContentArea and above the first Field, and then indent all the fields under the group, and it should magically work 🙂

          1. It works, just perfect!!! 🙂 Thank you so much. It is not my page, so I didn’t notice that there is no grup inside. Thanks once again.

  15. Hi,

    I have another question. When I try to update one field, I get a message: “A DotNet variable has not been instantiated. Attempting to call System.Reflection.MethodInfo.GetParameters in CodeUnit Generic Web Service Client: UPDATE”. It occurs in this line: “Parameters := Parameters.CreateInstance(GETDOTNETTYPE(Object),_Create.GetParameters().Length);”. I use your code for updating. Do you know the solution?
    Thanks for support.
    Damian

      1. @Damian: I know, but I don’t have time to look deeper into this right now. Can you provide the page object for which this is failing? It again seems to me to be some kind of metadata problem on the page.

  16. All above information is helped me lot.. Created web service and integrated with JD Edwards 2I order system in my organization.

  17. Hey!

    Thank you for the help! It was a perfect start for my project. I need to use some extra functions besides CRUD that Pages provide.

    If anybody needs to use Codeunit Web Services with the Page it is possible!

    First create a Codeunit and publish as an extension of the Page (It has to be published with the same Name, and the functions included need a record parameter of the same type in Page).

    Then few adjustments in Vjeko codeunit (defining the new methods) and voila!!

    Thank you!

  18. Thanks Vjeko,

    This is a great tool! It really helped! 😉

    The only thing I’m missing is the possibility to use delegates (not your fault!) to be able to bypass certificate validate in our dev.env. Now we use a custom .NET Class for that.

    Thanks,
    Johannes

  19. Hi,
    I have some problems when I use 2009 version.
    – I can’t use SETVALUE function with fields that are not Text or Code. When I use the function with a boolean field, for example, I have this error: “The expresion type Microsoft.Dynamics.Nav.Runtime.NavIndirectValue can´t be converted to a value Microsoft.Dynamccs.Nav.Runtime.NavText.”

    – I have a lot of errors like this “CustomerCard does not have a definition for the Territory_Code field”. Where can I find this definitions?

    Thank you!!

    1. David, can I see the code that causes the error when converting text to indirect value? I haven’t had any, I’ve even used it with the Option field. About the Territory Code field, do you actually have that field on the card, or is it just in the table?

      1. Thank you for your answer!

        This is my code:

        CONNECT(‘192.168.2.125:7099/DynamicsNavXX/WS/XX/Page/CustomerCard’);
        INIT;
        SETVALUE(‘No’,Customer.”No.”);
        SETVALUE(‘Name’,Customer.Name);
        SETVALUE(‘Search_Name’,Customer.”Search Name”);
        SETVALUE(‘Name_2’,Customer.”Name 2″);
        //SETVALUE(‘Our_Account__No_’,Customer.”Our Account No.”);
        //SETVALUE(‘Territory_Code’,Customer.”Territory Code”);
        //SETVALUE(‘Collection_Method’,Customer.”Collection Method”);
        //SETVALUE(‘Blocked’,Customer.Blocked);
        CREATE;

        The commented lines are the lines with error.

        The customer card have this fields in the General group with Visible = FALSE.

        Thanks

        1. Strange, it works for me on NAV 2013, but I don’t have an instance of 2009 available anymore to test it there. Can you try if you experience the same problem with 2013 as well?

          1. I found the problem. I call the codeunit from a report, and the Language of the report is not ‘ENU’.
            Now It is running!

            Thanks

  20. Hi Vjeko

    Great tool! I’m quite new to NAV and It helped a lot. Thank you.

    My question: Is there really no way in the WebService Page to access the values passed with SETVALUE?

    Best regards

    1. Thanks, Dan! I am not sure, though, what you mean by this question. SETVALUE is used to set the values, not to access the values. Can you please clarify?

      1. Hi Vjeko. Thanks for your answer. I try to clarify my problems

        Within my webservice page, I attempt to access the values, that I have set with SETVALUE.
        I have a simple Page that only writes the passed values as a new record into a table. In addition to the insert, i want to call a (cu) function using this values as parameters.

        E.g.:

        Type SubType SourceExpr
        Container ContentArea
        Group Group
        Field “Triggered by Company Name”
        Field “Company Name Called”
        Field “Webservice Called”
        Field “Parameter Type”
        Field Parameter

        OnInsertRecord(BelowxRec : Boolean) : Boolean

        cuItemMgt.SyncItemSCfromWH(“Triggered by Company Name”,”Parameter Type”,Parameter);

        The table insert works fine. No problem.
        But, no matter what I try, the function (SyncItemSCfromWH) always receives blanks or nulls. Even if I call it at the table’s OnInsert Trigger.

  21. Hello,

    I’ve been working with Generic Web Service for a while, but never faced this problem.

    I use Transfer Header Card.

    The error shows up on Create method of the codeunit. System.Reflection.RuntimeMoethodInfo.Invoke : Thge max length for a string is 10.

    I’m sending the transfer header no., and there should not be a problem with its length . In nav it is set to code 20…

    Maybe something related to parameters?? any guess?

    1. Håkan, yes – it is possible. I might do something like that in the future, but I can’t make any promisses. Principle is the same for all web services.

  22. Thanks for the article and code Vjeko – very valuable!

    But it looks like there is a problem with the READ function if you try to do a READ on a page where there is an option value, as one of the key fields.

    For test purpose I made a page with all fields from table 36 – Sales Header and tried this “code”:
    WITH WebService DO BEGIN
    CONNECT(‘http://…../Page/SH’);
    INIT;
    SETVALUE(‘Document_Type’, SH.”Document Type”::Order);
    SETVALUE(‘No’,’1500005′);
    READ;

    And this gives me the error message:
    A call to System.Reflection.RuntimeMethodInfo.Invoke failed with this message: Object of type ‘Document_Type’ cannot be converted to type ‘System.String’.

    It would be appriciated is any had seen/solved this issue.

    Thanks in advance.

    1. I can’t play with this right now, but I assume you can try with:
      SETVALUE(‘Document_Type’, FORMAT(SH.”Document Type”::Order));

    2. I’m using this tool a lot lately and there is a workaround for this.
      Instead of using SETVALUE use SETFILTER, so in your example do it like this:
      SETFILTER(‘Document_Type’, FORMAT(SH.”Document Type”::Order));
      SETFILTER(‘No’,’1500005′);

      Instead of using READ use READMULTIPLE but since you only need first record no need to REPEAT – UNTIL block so just add:

      IF READMULTIPLE THEN BEGIN
      END;

  23. Hi i had tried to call web service as you guide. i had used your code unit only. But while i am trying to call web service the line

    RequestStream := WebRequest.GetResponse().GetResponseStream();
    is giving me error – “a call to system.Net.HttpwebRequest.GetResponse failed with this message: the remote certificate is invalid according to the validation procedure.”

    I have also passed NAVUserid and password as you suggested that code is –
    ServiceInstance.Credentials := Credential.NetworkCredential(‘Admin’,’password’)
    but still same.

    can you please guide me for above issue.

  24. Hi Vjeko,
    I need this CodeUnit for the project that im working right now.

    The site mibuso.com is down because they are working in their new web and its imposible to find this in other site.

    Please, can you upload that CodeUnit in other place and share the link with us, for example in dropbox?

    Thanks in advance.

  25. Hi Vjeko,

    I’m wondering the same as some of the other poster’s above:

    Can this be used for web services with SSL?

    I’ve tried but it’s failing for me also.

    Thanks

  26. Dear Vjeko,

    Thank you for your greate tool.

    We are inserting Sales Invoice from one nav database to another using web service.
    The problem is we couldn’t figure out how to read from saleslines?
    There is a GETLINEVALUE function, but not sure how to read repeatedly within salesinvoice multiread.

    Could you please help on this issue?

  27. Also i tried to create sales invoice updating thought Web Service exactly same way in above example code
    “Creating a purchase order from a sales order”,
    Unfortunately, not working, showing below error:

    THE ERROR IS;

    Microsoft Dynamics NAV
    —————————

    SalesInvoice does not have lines.
    —————————
    OK
    —————————

    If i debug the code, it is stoping in line below on CU Generic Web Service Client

    AssertHasLines()
    IF NOT HASLINES THEN
    ERROR(Text006,Name);

    Need to initiate SalesLine for WS SalesInvoice to SETLINEVALUE? How? Or should i missing something?

    Please, give a guidance for my urgent issue..

  28. Hi, Vjeko, residents and guests!
    Please, tell me. Can I successfully work with this CU under Nav2009R2 Classic client (forms, reports, etc..)? Thank you for attention.

    1. I am not quite sure of it. It was not developed for 2009 R2, and it may not work correctly. It’s a very very old version of NAV and I hope you understand why I can’t spend time trying to figure out if this would work under it.

  29. thanks a lot Vjeko for the codeunit, it works well in NAV 2015. Is there any new version which support calling OData webservice from another system or another NAV?

    again, thank you very much.

    1. No, sorry, I never extended it to support this. However, for OData you don’t need anything this complex, it can be as simple as using JSON.NET with HttpWebRequest.

  30. Hi Vjeko,
    in regards to your Example “Creating a purchase order from a sales order”
    Is there is a way to Validate a field in the new Created Purchase order using your code?

    This will be really helpful.

    Best regards,
    Mohamed

  31. Hi Vjeko,
    Your code is working Perfectly with the English Client! but If I use the German client it will now worl due to an error on this line:
    Object := Enum.Parse(PropertyInfo.PropertyType,ValueText);

    Do you have any idea how can I over come such issue?

  32. Thanks Vjeko for this brilliant piece of NAV-code. We used it to synchronize information between two independent systems and it is working really well (and the code for consuming web services is easy to understand). We’ve had a problem with the memory consumption on the service-tiers, because every CONNECT will compile a new assembly in memory and they will never be released again. So I mixed the function CONNECT with your state-service trick, essentially storing the assembly, the related URI and the WSDL in those static dictionary on the Service Tier and reuse it, when nothing has changed.

    Thanks again.

  33. Hi Vjeko!
    Can you please write a topic to guide how to create a web service to get list of invoices/vendors and pay invoices in NAV 2009 (using codeunit)?

  34. Dear Vjeko,

    I have an error when I tried tu use the CU to update a Purch.Invoice Header through web service.

    This is my piece of code:
    WITH WebService DO BEGIN
    CONNECT(‘https://********/Page/FacturasPendientes’);
    INIT;
    SETVALUE(‘No’,”No.”);
    READ;
    SETVALUE(‘Approve’,FORMAT(“Approve”::Aprobado));
    UPDATE;
    END;

    Is strange because the error is only throwed in the second SETVALUE statment, this is the message:

    Microsoft Dynamics NAV
    —————————

    No se ha creado la instancia de la variable DotNet. Se está intentado llamar a System.Object.GetType en CodeUnit Generic Web Service Client: SetObjectValue
    —————————
    Aceptar
    —————————

    the translation is that the instance of the DotNet variable has not been created when the codeunit is trying to call the method SetObjectValue

    Any idea about that?

    1. Hm, I think it may have to do with the language. You are trying to assign the constant in Spanish, while .NET layer only sees English. Can you try to put “Approved” instead of “Aprobado”?

  35. Hi my dear partner

    you have some idea about how can i post purchase order? i need to do that from vb.net

    thank you in advance.

    1. You can’t do it using this example. This is only for page web services, not for codeunit web services. There are plenty of examples how to do what you need around the internet. Sorry that I don’t have much more time to delve into details now… 🙁

  36. I’ve used this codeunit by an conversion/migration from NAV2009 to NAV20016. In NAV2009 i’ve publised the data as webservice.
    Simple example of the code in NAV2016:

    ConvertSalesPerson()
    TimeVar[1] := TIME;
    Window.OPEN(Text001);
    WITH lWebService DO BEGIN
    CONNECT(‘http://dev02:7047/DynamicsNAV/WS/CompanyName/Page/SalesPersonWS’);
    Window.UPDATE(1,Salesperson.TABLECAPTION);
    Salesperson.DELETEALL(TRUE);
    SETFILTER(‘Job_Title’, ‘@inactief’);
    SETFILTER(‘Name’, ‘*@NIET GEBRUIKEN*’);
    IF READMULTIPLE THEN
    REPEAT
    CLEAR(Salesperson);
    UpdateWindow(2,Salesperson.Code);
    IF NOT Salesperson.GET(GETVALUE(‘Code’)) THEN BEGIN
    Salesperson.Code := GETVALUE(‘Code’);
    Salesperson.INSERT(TRUE);
    END;
    Salesperson.VALIDATE(Name, GETVALUE(‘Name’));
    Salesperson.VALIDATE(“Job Title”, GETVALUE(‘Job_Title’));
    Salesperson.MODIFY(TRUE);
    UNTIL NEXT = 0;
    END;
    Window.CLOSE;
    TimeVar[2] := TIME;
    LogDuration(‘Salesperson’,TimeVar[1],TimeVar[2]);

    It’s a nice and fast way to convert the data!

  37. Hi Vjeko,
    I try to use your codeunit to synchronize the item table of two company (on different server). But I get error with read-only field i.e. Last Date Modified. It is possible to know if a field is read only in Web Services, in this case for Item Card page?

    1. I try to use an table for exclusion fields (read-only fields), but I get another error when I modify the unit price: the error say that: Field CalcUnitPriceExclVAT is readonly! But CalcUnitPriceExclVAT is a global function on the Item table … Why I get this error?

      1. Because the page field you are trying to modify is apparently bound to a function. Rule is – if you can’t change it from UI, you can’t change it from Web Services.

    2. It’s not possible directly. You’d have to somehow access page metadata (it’s an XML document in the Object Metadata table) and read it from there. And perhaps also do this for the underlying table. It’s not trivial.

Leave a Reply