none
Problem on customizing soap header RRS feed

  • Question

  • Dear All,

    I create a custom message header class inherited from MessageHeader class, code segments are as follows:

            private const string PREFIX_CP = "cp";
    
            private const string NAMESPACE_CP = "http://www.gov.tw/CP/envelope";
    
            private const string NAMESPACE_SOAP = "http://schemas.xmlsoap.org/soap/envelope/";
    
    
    
            protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
    
            {
    
                base.OnWriteStartHeader(writer, MessageVersion.Soap12WSAddressingAugust2004);            
    
                
    
                writer.WriteXmlnsAttribute(PREFIX_CP, NAMESPACE_CP);
    
                //writer.WriteXmlnsAttribute("SOAP", NAMESPACE_SOAP);
    
    
    
                writer.WriteAttributeString(PREFIX_CP, "id", NAMESPACE_CP, "2.16.886.101.999999999.2097156.2.126.1");
    
                //writer.WriteAttributeString("SOAP", "mustUnderstand", NAMESPACE_SOAP, "1");           
    
    
    
            }
    
    
    
            protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    
            {
    
    
    
                //From
    
                writer.WriteStartElement(PREFIX_CP, "From", NAMESPACE_CP);
    
                writer.WriteString("2.16.886.101.999999999");
    
                writer.WriteEndElement();
    
    
    
                //To
    
                writer.WriteStartElement(PREFIX_CP, "To", NAMESPACE_CP);
    
                writer.WriteString("2.16.886.101.999999999");
    
                writer.WriteEndElement();
    
    
    
                //Service
    
                writer.WriteStartElement(PREFIX_CP, "Service", NAMESPACE_CP);
    
                writer.WriteString("AGM0000177");
    
                writer.WriteEndElement();
    
    
    
                //ServiceAgreementID
    
                writer.WriteStartElement(PREFIX_CP, "ServiceAgreementID", NAMESPACE_CP);
    
                writer.WriteString(Guid.NewGuid().ToString());
    
                writer.WriteEndElement();
    
    
    
                //ConversationID
    
                writer.WriteStartElement(PREFIX_CP, "ConversationID", NAMESPACE_CP);
    
                writer.WriteString(Guid.NewGuid().ToString());
    
                writer.WriteEndElement();
    
    
    
                this.WriteMessageInfo(writer);
    
    
    
                //DuplicateElimination 
    
                writer.WriteStartElement(PREFIX_CP, "DuplicateElimination ", NAMESPACE_CP);
    
                writer.WriteEndElement();
    
    
    
                //Description
    
                writer.WriteStartElement(PREFIX_CP, "Description", NAMESPACE_CP);
    
                writer.WriteString("This is a sample message");
    
                writer.WriteEndElement();
    
    
    
            }
    
    

    I get my soap header as follows:


    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
      <s:Header>
    
        <MessageHeader cp:id="2.16.886.101.999999999.2097156.2.126.1" xmlns:cp="http://www.gov.tw/CP/envelope">
    
          <cp:From>2.16.886.101.999999999</cp:From>
    
          <cp:To>2.16.886.101.999999999</cp:To>
    
          <cp:Service>AGM0000177</cp:Service>
    
          <cp:ServiceAgreementID>69f95c8d-765c-47f5-8fb8-932424eedf79</cp:ServiceAgreementID>
    
          <cp:ConversationID>38c70f89-1de2-4f5b-9ad3-0b767625712a</cp:ConversationID>
    
          <cp:MessageInfo>
    
            <cp:MessageID>d762b1f7-79d0-44e0-9cdd-c96d206c8d09</cp:MessageID>
    
            <cp:Timestamp>2009-05-14 04:32:17</cp:Timestamp>
    
            <cp:TimeToLive>2009-05-15 04:32:17</cp:TimeToLive>
    
            <cp:Version>1.0</cp:Version>
    
          </cp:MessageInfo>
    
          <cp:DuplicateElimination></cp:DuplicateElimination>
    
          <cp:Description>This is a sample message</cp:Description>
    
        </MessageHeader>
    
      </s:Header>
    
    
    

     

    I'd like to put a prefix to <MessageHeader> tag, i.e., <cp:MessageHeader>.

    And change the prefix of <s:Envelop> to <soap:Envelop>, and <s:Header> to <soap:Header>

    How should I do?

    Thanks in advance.

    Pete

    Monday, May 18, 2009 2:25 AM

Answers

  • -> I'd like to put a prefix to <MessageHeader> tag, i.e., <cp:MessageHeader>.
    Override System.ServiceModel.Channels.MessageHeader.OnWriteStartHeader() method.

    -> And change the prefix of <s:Envelop> to <soap:Envelop>, and <s:Header> to <soap:Header>

    Deriving from Message, and override its OnWriteStartEnvelope() method. And you could plug in your custom message into WCF processing pipeline using the idea I posted in this blog post:

    http://shevaspace.blogspot.com/2009/01/include-xml-declaration-in-wcf-restful.html

    Thanks
    Marco

    Another Paradigm Shift
    http://shevaspace.blogspot.com
    • Marked as answer by 城宮智 Thursday, May 21, 2009 2:32 AM
    Wednesday, May 20, 2009 2:25 AM
  • Okay, I think I find the root cause now, the WCF dispatch runtime will always ensure that the "mustUnderstand" SOAP headers are preprocessed by the any SOAP processing intermediaries(note that those intermediaries could reside in logical or physical boundaries). But in your case, you introduce a "mustUnderstand" header at the client, but does do anything useful at the service side to process this headers, and WCF dispatch runtime will always assume that if there is any "mustUnderstand" headers left before invoking the service operation, it will be an error. thus returning an generic SOAP fault, to workaround this issue, try doing any meaningful processing at the service side aka in IDispatchMessageInspector as follows:

    object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        int index = request.Headers.FindHeader("MessageHeader", namespaceCP);
        if (index >= 0)
        {
            var reader = request.Headers.GetReaderAtHeader(index);
            //Write code to process the "mustUnderstand" SOAP header.
            //Complete reading the headers.
            request.Headers.RemoveAt(index);
        }
       
        return null;
    }

    Hope this helps

    Another Paradigm Shift
    http://shevaspace.blogspot.com
    • Marked as answer by Marco Zhou Tuesday, May 26, 2009 6:48 AM
    Monday, May 25, 2009 9:57 AM

All replies

  • -> I'd like to put a prefix to <MessageHeader> tag, i.e., <cp:MessageHeader>.
    Override System.ServiceModel.Channels.MessageHeader.OnWriteStartHeader() method.

    -> And change the prefix of <s:Envelop> to <soap:Envelop>, and <s:Header> to <soap:Header>

    Deriving from Message, and override its OnWriteStartEnvelope() method. And you could plug in your custom message into WCF processing pipeline using the idea I posted in this blog post:

    http://shevaspace.blogspot.com/2009/01/include-xml-declaration-in-wcf-restful.html

    Thanks
    Marco

    Another Paradigm Shift
    http://shevaspace.blogspot.com
    • Marked as answer by 城宮智 Thursday, May 21, 2009 2:32 AM
    Wednesday, May 20, 2009 2:25 AM
  • It works. Thanks a million, Marco.

    Now I have another question. Have you ever seen the following situation?

    When customizing the soap header in the OnWriteStartHeaders() method, I use 'writer.WriteAttributeString(PREFIX_SOAP, "mustUnderstand", NAMESPACE_SOAP, "1");' to add an attribute to my header, which is called "MessageHeader".

    But an error appears on the screen saying that the receiver of the message cannot recognize the namespace I specify. The OnWriteStartHeaders() code snippet is as follows:

            protected override void OnWriteStartHeaders(XmlDictionaryWriter writer)
            {
                writer.WriteStartElement(PREFIX_SOAP, "Header", NAMESPACE_SOAP);
    
                //MessageHeader
                writer.WriteStartElement(PREFIX_CP, "MessageHeader", NAMESPACE_CP);
                writer.WriteAttributeString(PREFIX_CP, "id", NAMESPACE_CP, "2.16.886.101.999999999.2097156.2.126.1");
                writer.WriteAttributeString(PREFIX_SOAP, "mustUnderstand", NAMESPACE_SOAP, "1");
                writer.WriteXmlnsAttribute(PREFIX_CP, NAMESPACE_CP);
                writer.WriteXmlnsAttribute(PREFIX_SOAP, NAMESPACE_SOAP);
    
                //From
                writer.WriteElementString(PREFIX_CP, "From", NAMESPACE_CP, "2.16.886.101.999999999");
    
                //To
                writer.WriteElementString(PREFIX_CP, "To", NAMESPACE_CP, "2.16.886.101.999999999");
    
                //Service
                writer.WriteElementString(PREFIX_CP, "Service", NAMESPACE_CP, "AGM0000177");
    
                //ServiceAgreementID
                writer.WriteElementString(PREFIX_CP, "ServiceAgreementID", NAMESPACE_CP, Guid.NewGuid().ToString());
    
                //ConversationID
                writer.WriteElementString(PREFIX_CP, "ConversationID", NAMESPACE_CP, Guid.NewGuid().ToString());
    
                this.WriteMessageInfo(writer);
    
                //DuplicateElimination
                writer.WriteElementString(PREFIX_CP, "DuplicateElimination", NAMESPACE_CP, string.Empty);
    
                //Description
                writer.WriteElementString(PREFIX_CP, "Description", NAMESPACE_CP, "This is a sample message");
    
                writer.WriteEndElement();
            }


    Additionally, if I take out the line "writer.WriteAttributeString(PREFIX_SOAP, "mustUnderstand", NAMESPACE_SOAP, "1");", the error disappears.

    Thanks again.



    Pete

    Thursday, May 21, 2009 2:32 AM
  • Have you grabbed the final SOAP message? you could post the SOAP message here for us to analyze, it might be simply a XML namespace missing or mismatch issue.

    You could capture the SOAP message either using WCF tracing or write a custom IClientMessageInspector .

    Thanks
    Marco
    Another Paradigm Shift
    http://shevaspace.blogspot.com
    • Edited by Marco Zhou Thursday, May 21, 2009 3:17 AM bad typo
    Thursday, May 21, 2009 3:17 AM
  • Here's the SOAP message I captured from the client.

    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    
      <soap:Header>
    
        <cp:MessageHeader cp:id="2.16.886.101.999999999.2097156.2.126.1" soap:mustUnderstand="1" xmlns:cp="http://www.gov.tw/CP/envelope">
    
          <cp:From>2.16.886.101.999999999</cp:From>
    
          <cp:To>2.16.886.101.999999999</cp:To>
    
          <cp:Service>AGM0000177</cp:Service>
    
          <cp:ServiceAgreementID>e18d7857-6588-4ed0-8d09-b3078aace782</cp:ServiceAgreementID>
    
          <cp:ConversationID>5ffea0c8-bb3b-404d-aaae-76dbf5f7f8bd</cp:ConversationID>
    
          <cp:MessageInfo>
    
            <cp:MessageID>88caabf9-65b9-4b1c-bdf0-2d78c68b89ad</cp:MessageID>
    
            <cp:Timestamp>2009-05-21 01:36:32</cp:Timestamp>
    
            <cp:TimeToLive>2009-05-22 01:36:32</cp:TimeToLive>
    
            <cp:Version>1.0</cp:Version>
    
            </cp:MessageInfo>
    
          <cp:DuplicateElimination></cp:DuplicateElimination>
    
          <cp:Description>This is a sample message</cp:Description>
    
        </cp:MessageHeader>
    
        <ActivityId CorrelationId="87fcc931-f814-414c-9b2a-98ea3f9390b4" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">c6bfd3c1-8516-43e8-8fa4-0644201fe7b3</ActivityId>
    
      </soap:Header>
    
    </soap:Envelope>
    
    
    I found it weird because the 'soap:mustUnderstand' attribute was still being added even though the machine told me that there's an error.

    By the way, if I change the attribute name to "mustUnderstand2", there's no error.


    Pete
    • Edited by 城宮智 Thursday, May 21, 2009 5:56 AM
    Thursday, May 21, 2009 5:53 AM
  • Okay, I think in order to specify that a header should be "mustUnderstood" by upstream SOAP actor, you need to derive from the MessageHeader class as follows:

    public class CustomHeader : MessageHeader
    {
        public override bool MustUnderstand
        {
            get
            {
                return true;
            }
        }
    }

    Thanks
    Marco
    Another Paradigm Shift
    http://shevaspace.blogspot.com
    Thursday, May 21, 2009 6:14 AM
  • Now I derive from the MessageHeader and set the 'MustUnderstand' property to be true. But the error message still appears.

    Interestingly, if I set the 'MessageVersion' attribute to 'Soap12WSAddressingAugust2004', it works! (I don't know why)

    The code segment of the derived class is as follows

        public class ClientMessageHeader : MessageHeader
        {
    
            private const string PREFIX_CP = "cp";
            private const string NAMESPACE_CP = "http://www.gov.tw/CP/envelope";
            private const string NAMESPACE_SOAP = "http://schemas.xmlsoap.org/soap/envelope/";
    
            protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
            {
                base.OnWriteStartHeader(writer, MessageVersion.Soap12WSAddressingAugust2004);
                writer.WriteAttributeString(PREFIX_CP, "id", NAMESPACE_CP, "2.16.886.101.999999999.2097156.2.126.1");
                writer.WriteXmlnsAttribute(PREFIX_CP, NAMESPACE_CP);
            }
    
            protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
            {
    
                //From
                writer.WriteElementString(PREFIX_CP, "From", NAMESPACE_CP, "2.16.886.101.999999999");
                //writer.WriteStartElement(PREFIX_CP, "From", NAMESPACE_CP);
                //writer.WriteString("2.16.886.101.999999999");
                //writer.WriteEndElement();
    
                //To
                writer.WriteElementString(PREFIX_CP, "To", NAMESPACE_CP, "2.16.886.101.999999999");
    
                //Service
                writer.WriteElementString(PREFIX_CP, "Service", NAMESPACE_CP, "AGM0000177");
    
                //ServiceAgreementID
                writer.WriteElementString(PREFIX_CP, "ServiceAgreementID", NAMESPACE_CP, Guid.NewGuid().ToString());
    
                //ConversationID
                writer.WriteElementString(PREFIX_CP, "ConversationID", NAMESPACE_CP, Guid.NewGuid().ToString());
    
                this.WriteMessageInfo(writer);
    
                //DuplicateElimination
                writer.WriteElementString(PREFIX_CP, "DuplicateElimination", NAMESPACE_CP, string.Empty);
    
                //Description
                writer.WriteElementString(PREFIX_CP, "Description", NAMESPACE_CP, "This is a sample message");
    
            }
    
            public override string Name
            {
                get { return "cp:MessageHeader"; }
                //get { return "MessageHeader"; }
            }
    
            public override string Namespace
            {
                get { return ""; }
            }
            
            public override bool MustUnderstand
            {
                get
                {
                    return true;
                }
            }
    
            public ClientMessageHeader()
            {
            }
    
            private void WriteMessageInfo(XmlDictionaryWriter writer)
            {
                //MessageInfo
                writer.WriteStartElement(PREFIX_CP, "MessageInfo", NAMESPACE_CP);
    
                //MessageID
                writer.WriteElementString(PREFIX_CP, "MessageID", NAMESPACE_CP, Guid.NewGuid().ToString());
    
                //Timestamp
                writer.WriteElementString(PREFIX_CP, "Timestamp", NAMESPACE_CP, DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss"));
    
                //TimeToLive
                writer.WriteElementString(PREFIX_CP, "TimeToLive", NAMESPACE_CP, DateTime.Now.AddDays(1).ToString("yyyy-MM-dd hh:mm:ss"));
    
                //Version
                writer.WriteElementString(PREFIX_CP, "Version", NAMESPACE_CP, "1.0");
    
                writer.WriteEndElement();
            }
        }

    Then in the SOAP message, I can get two attributes in the <cp:MessageHeader> tag, which are 'a:mustUnderstand="1"' and 'xmlns:a="http://www.w3.org/2003/05/soap-envelope"'.

    But it still is not the result I want. I hope it will be 'soap:mustUnderstand="1"' and 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"'.

    Any idea? Thanks.


    Pete
    • Edited by 城宮智 Thursday, May 21, 2009 8:43 AM
    Thursday, May 21, 2009 8:29 AM
  • Could you please provide a small, complete and ready-to-run example and send it to me at v-mazho at microsoft dot com for repro? I should be able to figure out what's going on after looking into your code.

     

    Thanks

    Marco


    Another Paradigm Shift
    http://shevaspace.blogspot.com
    Thursday, May 21, 2009 10:07 AM
  • Hi, Marco. I've sent you my code example. Thanks for your help.
    Friday, May 22, 2009 4:09 AM
  • What's the necessary step I need to follow to reproduce the issue you are encountering, when I run the code you sent after a little config tweaking, I don't see any exception/error thrown.

    Thanks
    Marco
    Another Paradigm Shift
    http://shevaspace.blogspot.com
    Friday, May 22, 2009 9:17 AM
  • You can modify the line 24 'base.OnWriteStartHeader(writer, MessageVersion.Soap12WSAddressingAugust2004);' in the 'ClientMessageHeader.cs' to             'base.OnWriteStartHeader(writer, messageVersion);', then you'll see the error I mentioned.


    Pete
    Friday, May 22, 2009 11:16 AM
  • Sorry, I still cannot reproduce this error, I think I can close this thread right now since the second issue you are encountering is a separate issue. I will send the code back to you for further confirmation.

    Thanks
    Marco
    Another Paradigm Shift
    http://shevaspace.blogspot.com
    Monday, May 25, 2009 3:26 AM
  • Okay, I think I find the root cause now, the WCF dispatch runtime will always ensure that the "mustUnderstand" SOAP headers are preprocessed by the any SOAP processing intermediaries(note that those intermediaries could reside in logical or physical boundaries). But in your case, you introduce a "mustUnderstand" header at the client, but does do anything useful at the service side to process this headers, and WCF dispatch runtime will always assume that if there is any "mustUnderstand" headers left before invoking the service operation, it will be an error. thus returning an generic SOAP fault, to workaround this issue, try doing any meaningful processing at the service side aka in IDispatchMessageInspector as follows:

    object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        int index = request.Headers.FindHeader("MessageHeader", namespaceCP);
        if (index >= 0)
        {
            var reader = request.Headers.GetReaderAtHeader(index);
            //Write code to process the "mustUnderstand" SOAP header.
            //Complete reading the headers.
            request.Headers.RemoveAt(index);
        }
       
        return null;
    }

    Hope this helps

    Another Paradigm Shift
    http://shevaspace.blogspot.com
    • Marked as answer by Marco Zhou Tuesday, May 26, 2009 6:48 AM
    Monday, May 25, 2009 9:57 AM
  • Thanks, Marco. You really help a lot.
    I've been working on this for a long time, and it's finally solved. Thanks again.

    Pete
    Tuesday, May 26, 2009 3:50 AM
  • Marco, 

            I am aving same problem with 'MustUnderstand' error for last few weeks.  I saw your solution of 'IDispatchMessageInspector' above.  I would really appreciate if you can help me here.

    Can you please provide an example of how to write class that is inherited using interface - IDispatchMessageInspector.  

    Here is the Response message I get back from server:

    <

     

    soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">

    <

     

    soap:Header>

    <

     

    a:Action soap:mustUnderstand="true" xmlns:a="http://www.w3.org/2005/08/addressing">http://resultmanager.ws.informatics.siemens.com/OrdersAndResultService/getTests</a:Action>

    <

     

    wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">

    <

     

    wsse:UsernameToken>

    <

     

    wsse:Username>

    <!--

     

    Removed-->

    </

     

    wsse:Username>

    <

     

    wsse:Password>

    <!--

     

    Removed-->

    </

     

    wsse:Password>

    </

     

    wsse:UsernameToken>

    </

     

    wsse:Security>

    <

     

    a:MessageID xmlns:a="http://www.w3.org/2005/08/addressing">urn:uuid:8a3dc133-ef07-4e8a-bbff-a6dec77260ef</a:MessageID>

    <

     

    a:ReplyTo xmlns:a="http://www.w3.org/2005/08/addressing">

    <

     

    a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>

    </

     

    a:ReplyTo>

    <

     

    a:To soap:mustUnderstand="true" xmlns:a="http://www.w3.org/2005/08/addressing">https://172.17.61.48/LabManager/webservice/ResultManager/OrdersAndResultService</a:To>

    </

     

    soap:Header>

    <

     

    soap:Body>

    <

     

    soap:Fault>

    <

     

    soap:Code>

    <

     

    soap:Value>soap:MustUnderstand</soap:Value>

    </

     

    soap:Code>

    <

     

    soap:Reason>

    <

     

    soap:Text xml:lang="en">MustUnderstand headers: [{http://www.w3.org/2005/08/addressing}Action, {http://www.w3.org/2005/08/addressing}To] are not understood.</soap:Text>

    </

     

    soap:Reason>

    </

     

    soap:Fault>

    </

     

    soap:Body>

    </

    soap:Envelope>

     

    soap:Envelope>

    soap:Envelope>



    'SimpleCredentialsHeader.cs' 


    namespace myNamespace

    { public class SimpleCredentialsHeader : MessageHeader { private const string PREFIX_CP = "wsse"; public string Username { get; set; } public string Password { get; set; } public SimpleCredentialsHeader() { Username = "Anonymous"; Password = string.Empty; } public override string Name { get { return "wsse:Security"; } } public override string Namespace { get { return null; } } public override bool MustUnderstand { get { return false; } } protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion) { base.OnWriteStartHeader(writer, messageVersion); writer.WriteXmlnsAttribute(PREFIX_CP, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"); } protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion) { writer.WriteStartElement(PREFIX_CP, "UsernameToken", null); writer.WriteElementString(PREFIX_CP, "Username", null, "Myusername"); writer.WriteElementString(PREFIX_CP, "Password", null, "Mypassword"); writer.WriteEndElement(); } } }
    Client code: 'MainForm.cs'






    using (OperationContextScope opScope = new OperationContextScope((IContextChannel)l_client.InnerChannel)) { OperationContext op = OperationContext.Current; SimpleCredentialsHeader credheader = new SimpleCredentialsHeader(); op.OutgoingMessageHeaders.Add(credheader); getTestsOutput out1 = l_client.getTests("MytestName", null);




    }

     

     


     

    Tuesday, August 4, 2009 6:50 PM
  • In this following link neither MessageHeader.OnWriteStartHeader() nor OnWriteStartEnvelope() is overridden. So how can I change prefix to <MessageHeader> tag, i.e., <cp:MessageHeader>. Please revert.

    http://shevaspace.blogspot.com/2009/01/include-xml-declaration-in-wcf-restful.html


    Hello

    Monday, August 5, 2013 9:50 AM
  • I am getting SOAP header as following, but it is not working. What would be the problem in this?

    v4:sessionHeader [ s:mustUnderstand=1 xmlns=http://api.bronto.com/v4 xmlns:v4=http://api.bronto.com/v4 ]

    <sessionId>07c155ef-3797-4a69-b1bb-95e263e352c9</sessionId>


    Hello

    Monday, August 5, 2013 10:43 AM
  • My problem also resolved by overriding MustUnderstand property. Thanks a lot.

    Hello

    Monday, August 5, 2013 11:20 AM