paypal / ipn-code-samples

Other
562 stars 486 forks source link

Please provide an example for ASP.NET Core 2.0+ #121

Closed drabaco closed 6 years ago

drabaco commented 6 years ago

General information

Issue description

Please add a working example for ASP.NET Core 2.0+. The C# example is for classic asp.net, is not working and many object does not exist.

Thanks

overint commented 6 years ago

Hi drabaco, You are welcome to submit a PR with a new example. Thanks.

drabaco commented 6 years ago

Thank you for your reply.

I'm a newbie, that's why i'm asking a working example ...

MalpenZibo commented 6 years ago

Something like this (NOT TESTED)

[Produces("application/json")]
[Route("api/IPN")]
public class IPNController : Controller
{
    [HttpPost]
    public IActionResult Receive()
    {
        //Store the IPN received from PayPal
        LogRequest(Request);

        //Fire and forget verification task
        Task.Run(() =>  VerifyTask(Request));

        //Reply back a 200 code
        return Ok();
    }

    private void VerifyTask(HttpRequest ipnRequest)
    {
        string verificationResponse = string.Empty;

        try
        {
            var verificationRequest = WebRequest.Create("https://www.sandbox.paypal.com/cgi-bin/webscr");

            //Set values for the verification request
            verificationRequest.Method = "POST";
            verificationRequest.ContentType = "application/x-www-form-urlencoded";
            string strRequest;
            using( StreamReader reader = new StreamReader( Request.Body, Encoding.ASCII ) )
            {  
                strRequest = reader.ReadToEnd();
            }

            //Add cmd=_notify-validate to the payload
            strRequest = "cmd=_notify-validate&" + strRequest;
            verificationRequest.ContentLength = strRequest.Length;

            //Attach payload to the verification request
            using( StreamWriter writer = new StreamWriter( verificationRequest.GetRequestStream(), Encoding.ASCII ) )
            {
                writer.Write( strRequest );
            }

            //Send the request to PayPal and get the response
            using( StreamReader reader = new StreamReader( verificationRequest.GetResponse().GetResponseStream() ) )
            {
                verificationResponse = reader.ReadToEnd();
            }
        }
        catch( Exception exception )
        {
            //Capture exception for manual investigation
        }

        ProcessVerificationResponse(verificationResponse);
    }

    private void LogRequest(HttpRequest request)
    {
        // Persist the request values into a database or temporary data store
    }

    private void ProcessVerificationResponse(string verificationResponse)
    {
        if (verificationResponse.Equals("VERIFIED"))
        {
            // check that Payment_status=Completed
            // check that Txn_id has not been previously processed
            // check that Receiver_email is your Primary PayPal email
            // check that Payment_amount/Payment_currency are correct
            // process payment
        }
        else if (verificationResponse.Equals("INVALID"))
        {
            //Log for manual investigation
        }
        else
        {
            //Log error
        }
    }
}
drabaco commented 6 years ago

Thank you MalpenZibo, i realized something like you, but i have a main problem (in both implementation):

When httpRequest arrive to VerifyTask is already disposed: HERE

using( StreamReader reader = new StreamReader( Request.Body, Encoding.ASCII ) ) //REQUEST IS DISPOSED
            {  
                strRequest = reader.ReadToEnd();
            }

so it throw exception.

: System.ObjectDisposedException: Cannot access a disposed object. Object name: 'FrameRequestStream'. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.FrameRequestStream.ValidateState(CancellationToken cancellationToken) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.FrameRequestStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.FrameRequestStream.Read(Byte[] buffer, Int32 offset, Int32 count) at System.IO.StreamReader.ReadBuffer() at System.IO.StreamReader.ReadToEnd() at xxxxxxx.Controllers.IPNController.VerifyTask(HttpRequest ipnRequest)

In LogRequest the httpRequest is readable, so if i add a log there the httpRequest get logged.

It seems that the request got disposed around here:

//Fire and forget verification task
Task.Run(() =>  VerifyTask(Request));
MalpenZibo commented 6 years ago

Try to use HttpRequest ipnRequest from VerifyTask function argument OR

[Produces("application/json")]
[Route("api/IPN")]
public class IPNController : Controller
{
    private class IPNContext
    {
        public HttpRequest IPNRequest { get; set; }

        public string RequestBody { get; set; }

        public string Verification { get; set; } = String.Empty;
    }

    public IActionResult Receive()
    {
        IPNContext context = new IPNContext()
        {
            IPNRequest = Request
        };

        LogRequest( context );

        //Fire and forget verification task
        Task.Run( () => VerifyTask( context ) );

        //Reply back a 200 code
        return Ok();
    }

    private void VerifyTask( IPNContext ipnContext )
    {
        try
        {
            var verificationRequest = WebRequest.Create("https://www.sandbox.paypal.com/cgi-bin/webscr");

            //Set values for the verification request
            verificationRequest.Method = "POST";
            verificationRequest.ContentType = "application/x-www-form-urlencoded";
            using( StreamReader reader = new StreamReader( ipnContext.IPNRequest.Body, Encoding.ASCII ) )
            {  
                ipnContext.RequestBody = reader.ReadToEnd();
            }

            //Add cmd=_notify-validate to the payload
            string strRequest = "cmd=_notify-validate&" + ipnContext.RequestBody;
            verificationRequest.ContentLength = strRequest.Length;

            //Attach payload to the verification request
            using( StreamWriter writer = new StreamWriter( verificationRequest.GetRequestStream(), Encoding.ASCII ) )
            {
                writer.Write( strRequest );
            }

            //Send the request to PayPal and get the response
            using( StreamReader reader = new StreamReader( verificationRequest.GetResponse().GetResponseStream() ) )
            {
                ipnContext.Verification = reader.ReadToEnd();
            }
        }
        catch( Exception exception )
        {
            //Capture exception for manual investigation
        }

        ProcessVerificationResponse( ipnContext );
    }

    private void LogRequest( IPNContext ipnContext )
    {
        // Persist the request values into a database or temporary data store
    }

    private void ProcessVerificationResponse( IPNContext ipnContext )
    {
        if( ipnContext.Verification.Equals("VERIFIED") )
        {
            // check that Payment_status=Completed
            // check that Txn_id has not been previously processed
            // check that Receiver_email is your Primary PayPal email
            // check that Payment_amount/Payment_currency are correct
            // process payment
        }
        else if( ipnContext.Verification.Equals("INVALID") )
        {
            //Log for manual investigation
        }
        else
        {
            //Log error
        }
    }
}
MalpenZibo commented 6 years ago

If the error persists the only way is to read the Request content body before

//Fire and forget verification task
Task.Run( () => VerifyTask( context ) );

//Reply back a 200 code
return Ok();

Asp.Net Core probably disposes all request data after Ok() response and because Task.Run is not awaited when you read Request.Body the request is already disposed.

You can try with

private class IPNContext
{
    public HttpRequest IPNRequest { get; set; }

    public string RequestBody { get; set; }

    public string Verification { get; set; } = String.Empty;
}

public IActionResult Receive()
{
    IPNContext context = new IPNContext()
    {
        IPNRequest = Request
    };

    using( StreamReader reader = new StreamReader( ipnContext.IPNRequest.Body, Encoding.ASCII ) )
    {  
        ipnContext.RequestBody = reader.ReadToEnd();
    }

    LogRequest( context );

    //Fire and forget verification task
    Task.Run( () => VerifyTask( context ) );

    //Reply back a 200 code
    return Ok();
}
drabaco commented 6 years ago

Yes, i'm trying exactly what you suggest.

All request data get disposed immediatly after Ok(), and Task.Run is not awaited.

Reading at first the request and incapsultating into ipnContext.RequestBody like you suggested seem to work.

I'm testing it right now. Thanks

drabaco commented 6 years ago

It seems to work like expected.

Thanks MalpenZibo for your precious support.

overint commented 6 years ago

@MalpenZibo Any chance you would be able to create a PR to add this to the sample repo? Thanks!

MalpenZibo commented 6 years ago

I will do it as soon as I can

overint commented 6 years ago

Thanks @MalpenZibo 😄