timheuer / alexa-skills-dotnet

An Amazon Alexa Skills SDK for .NET
MIT License
547 stars 108 forks source link

Request class being abstract gives errors when using with Azure Functions #99

Closed devedse closed 5 years ago

devedse commented 6 years ago

One of the members of SkillRequest is named Request. Which is an abstract class. When using this code in Azure Functions, C# can't deserialize the JSON to an abstract class.

My proposal would be to remove the abstract part and make it a normal class.

Error:

2018-06-28T14:04:13.298 [Error] System.Private.CoreLib: Exception while executing function: AccountLinking2. Microsoft.Azure.WebJobs.Host: Exception binding parameter 'req'. Newtonsoft.Json: Could not create an instance of type Alexa.NET.Request.Type.Request. Type is an interface or abstract class and cannot be instantiated. Path 'request.type', line 30, position 9.
2018-06-28T14:04:13.416 [Error] Executed 'AccountLinking2' (Failed, Id=a00304f2-5654-4102-b5e0-0cc2f9bd4fa7)

Edit: I created a pull request to fix this issue #100 . If you accept this, can you also make sure a new NuGet package will be published?

Edit2: This pull request probably isn't the right way to solve it.

stoiveyp commented 6 years ago

Hi @devedse

Due to the way we deserialize Alexa requests, there isn't a scenario I'm aware of where using the base Request object would generate a valid representation of a skill request.

Can I ask which request type you're getting back from Alexa which is causing this issue for you? Or a snippet of the JSON you're receiving (anonymised of course) would be even more helpful.

Thank you

devedse commented 6 years ago

I tried to call the azure function with this JSON:

{
    "version": "1.0",
    "session": {
        "new": true,
        "sessionId": "amzn1.echo-api.session.b76cb7b6-5c86-4238-a115-5c85e54abdbb",
        "application": {
            "applicationId": "amzn1.ask.skill.7539a510-6806-416a-a178-a28e16bdfe8f"
        },
        "user": {
            "userId": "amzn1.ask.account.AG4PZGMCSU45ZMXLLNK2OGMNFIQRUQ7YAPM5NZTA4B7OPY56I2PLDX7AWNI5WALLFIMFADJBIB3WT66QMNRF7IVSTIOHTCUUMIIAQ26ZU3LOV3VO3X4IOXS6M2SK47JJX4ROP427WM7DKYVFIIMRDVNCMCCGKJE3JXXBEBZOPROPV3RRDXIY7B6LL3IXVWN657U2ZF64H5WA76Q"
        }
    },
    "context": {
        "System": {
            "application": {
                "applicationId": "amzn1.ask.skill.7539a510-6806-416a-a178-a28e16bdfe8f"
            },
            "user": {
                "userId": "amzn1.ask.account.AG4PZGMCSU45ZMXLLNK2OGMNFIQRUQ7YAPM5NZTA4B7OPY56I2PLDX7AWNI5WALLFIMFADJBIB3WT66QMNRF7IVSTIOHTCUUMIIAQ26ZU3LOV3VO3X4IOXS6M2SK47JJX4ROP427WM7DKYVFIIMRDVNCMCCGKJE3JXXBEBZOPROPV3RRDXIY7B6LL3IXVWN657U2ZF64H5WA76Q"
            },
            "device": {
                "deviceId": "amzn1.ask.device.AGPBVEN5YHL52MJERXS4CCTOWFTPRHS3JOYQVJ7SF7Z427GM5BZS22LIOHW7UYUYJLSDDYAJHYRUSDYYR4PJQNPIVONAZOFRJSCHXHJKFSIJKCS2XGWYD5G3ZAHT5ALMZOH2XXTLDK3QNIFRV4X2JODQY63Q",
                "supportedInterfaces": {}
            },
            "apiEndpoint": "https://api.eu.amazonalexa.com",
            "apiAccessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2FwaS5hbWF6b25hbGV4YS5jb20iLCJpc3MiOiJBbGV4YVNraWxsS2l0Iiwic3ViIjoiYW16bjEuYXNrLnNraWxsLjc1MzlhNTEwLTY4MDYtNDE2YS1hMTc4LWEyOGUxNmJkZmU4ZiIsImV4cCI6MTUzMDE5ODAzOCwiaWF0IjoxNTMwMTk0NDM4LCJuYmYiOjE1MzAxOTQ0MzgsInByaXZhdGVDbGFpbXMiOnsiY29uc2VudFRva2VuIjpudWxsLCJkZXZpY2VJZCI6ImFtem4xLmFzay5kZXZpY2UuQUdQQlZFTjVZSEw1Mk1KRVJYUzRDQ1RPV0ZUUFJIUzNKT1lRVko3U0Y3WjQyN0dNNUJaUzIyTElPSFc3VVlVWUpMU0REWUFKSFlSVVNEWVlSNFBKUU5QSVZPTkFaT0ZSSlNDSFhISktGU0lKS0NTMlhHV1lENUczWkFIVDVBTE1aT0gyWFhUTERLM1FOSUZSVjRYMkpPRFFZNjNRIiwidXNlcklkIjoiYW16bjEuYXNrLmFjY291bnQuQUc0UFpHTUNTVTQ1Wk1YTExOSzJPR01ORklRUlVRN1lBUE01TlpUQTRCN09QWTU2STJQTERYN0FXTkk1V0FMTEZJTUZBREpCSUIzV1Q2NlFNTlJGN0lWU1RJT0hUQ1VVTUlJQVEyNlpVM0xPVjNWTzNYNElPWFM2TTJTSzQ3SkpYNFJPUDQyN1dNN0RLWVZGSUlNUkRWTkNNQ0NHS0pFM0pYWEJFQlpPUFJPUFYzUlJEWElZN0I2TEwzSVhWV042NTdVMlpGNjRINVdBNzZRIn19.Wy0nX4mHd31Mw_tEHzZMO4voBuatWr-GJte5x-0BKnVgQL6esffS3W_CTe5EsaAIlpkFZLworBW710v-t7D9AhPkD7VYG6xFAXjrtdtREi49857JVXuHmk_hACqOFLOHQkqdv0l9Xklxdmhg9Yabd2PW9F3LxtFU4eZ-FqTZG8HUcbVnl5nycWalckUOAMHe6AFIt6X1_dWWWD8DHqMfQGi4n6aG3ZioiMq_DULEnhYRiezsk9nkESNnnBfNREbQhHdw5vvcRnoYPERRlWvkP4mBWvm2O03geS6Mt-j0ouuIA7OS52mBYiruHV8CDgGK5UvqkXBFfbDDA9kik9nevQ"
        }
    },
    "request": {
        "type": "LaunchRequest",
        "requestId": "amzn1.echo-api.request.eabb800a-5c31-4ea5-a637-24c1914a5956",
        "timestamp": "2018-06-28T14:00:38Z",
        "locale": "en-US",
        "shouldLinkResultBeReturned": false
    }
}

It seems however that indeed as you said there's something going wrong with the deserialization of the Request types.

stoiveyp commented 6 years ago

Okay that's great - thank you. So the desired output should be that the property would have a LaunchRequest object in it.

First thing for me to check - are you using JSON.NET to Deserialize the JSON?

stoiveyp commented 6 years ago

Sorry - that was a silly question, your original error message had Newtonsoft in it, I just didn't scroll enough.

stoiveyp commented 6 years ago

Are you able to provide the JSON conversion code you're using in the function to get from the json to the SkillRequest object? Thanks.

devedse commented 6 years ago

Well with Azure Functions this all happens behind the scene I assume. So this is basically my code:

    public static class Function1
    {
        [FunctionName("Alexa")]
        public static async Task<SkillResponse> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] [FromBody]SkillRequest req, TraceWriter log)
        {
            SkillResponse response = null;
            if (req?.Session?.User?.AccessToken != null)
            {
                ClaimsPrincipal principal = await Security.ValidateTokenAsync(req.Session.User.AccessToken);
                if (principal != null)
                {
                    PlainTextOutputSpeech outputSpeech = new PlainTextOutputSpeech();
                    string firstName = (req.Request as IntentRequest)?.Intent.Slots.FirstOrDefault(s => s.Key == "FirstName").Value?.Value;
                    outputSpeech.Text = "Hello " + firstName;
                    response = ResponseBuilder.Tell(outputSpeech);
                }
            }
            return response;
        }
    }
}

I followed the tutorial from Microsoft here: https://blogs.msdn.microsoft.com/premier_developer/2018/01/25/amazon-alexa-skills-authenticated-by-azure-active-directory-and-backed-by-an-azure-function/

Basically just following all their steps results in the error I described.

stoiveyp commented 6 years ago

Hi @devedse - sorry I haven't had chance to look at this until this morning.

I've put a working example here https://github.com/stoiveyp/Alexa.NET.Samples/tree/master/AzureFunction - worth mentioning it didn't work when the skill request object was actually named request, hence it called req

devedse commented 6 years ago

@stoiveyp, I'll check out the sample thanks. I indeed also ran into that error and submitted a Github post here: https://github.com/Azure/azure-functions-host/issues/3071 I'm quite stumped on how that error happens.

damienmccauley commented 6 years ago

Hi, I am having the same issue from both my application and when using the sample code you provided @stoiveyp do we have any more information / work arounds? I was wondering if the json payload from amazon changed to simply be type request as opposed to a concrete type in Alexa.Net? Are there any plans to change request to a concrete type? (I noticed that this was implemented and then abandoned. ) Any information greatly appreciated.

EDIT1: Some supporting information...when passing in a payload like this { "version": "1.0", "session": { "new": true, "sessionId": "amzn1.echo-api.session.444c8b86-66eb-4a38-b2a6-48858c565e5d", "application": { "applicationId": "amzn1.ask.skill.4b5e4818-e543-48a3-a4eb-3045a08165e9" }, "user": { "userId": "amzn1.ask.account.QWERTY" } }, "context": { "System": { "application": { "applicationId": "amzn1.ask.skill.4b5e4818-e543-48a3-a4eb-3045a08165e9" }, "user": { "userId": "amzn1.ask.account.QWERTY" }, "device": { "deviceId": "amzn1.ask.device.AHX7C7S4HTLKPV3SXRIZA2N5IV4PNRWL2Y5TFG7DL3XLRIUFJIV4GZMCVDATZ6HUJGFRDLZFRF7KI7O5NMIFQPTATROUIC3ASZ4GMOAZDEERZIP3PRRB3XCZDW6LZFKXVX5ZVKY6BBTUWDK7X34DGQ7BT4MGDLEX7NIT6L4XRTMDA4BCZUCXC", "supportedInterfaces": {} }, "apiEndpoint": "https://api.amazonalexa.com", "apiAccessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2FwaS5hbWF6b25hbGV4YS5jb20iLCJpc3MiOiJBbGV4YVNraWxsS2l0Iiwic3ViIjoiYW16bjEuYXNrLnNraWxsLjRiNWU0ODE4LWU1NDMtNDhhMy1hNGViLTMwNDVhMDgxNjVlOSIsImV4cCI6MTUzNzE5NTk3MSwiaWF0IjoxNTM3MTkyMzcxLCJuYmYiOjE1MzcxOTIzNzEsInByaXZhdGVDbGFpbXMiOnsiY29uc2VudFRva2VuIjpudWxsLCJkZXZpY2VJZCI6ImFtem4xLmFzay5kZXZpY2UuQUhYN0M3UzRIVExLUFYzU1hSSVpBMk41SVY0UE5SV0wyWTVURkc3REwzWExSSVVGSklWNEdaTUNWREFUWjZIVUpHRlJETFpGUkY3S0k3TzVOTUlGUVBUQVRST1VJQzNBU1o0R01PQVpERUVSWklQM1BSUkIzWENaRFc2TFpGS1hWWDVaVktZNkJCVFVXREs3WDM0REdRN0JUNE1HRExFWDdOSVQ2TDRYUlRNREE0QkNaVUNYQyIsInVzZXJJZCI6ImFtem4xLmFzay5hY2NvdW50LkFHQk1NTjdZR0dSSEhTSFhGTUtFR1lDSE5LSk9WTFZYTU1SRzRBN0pTNU9aTUFWTUZNVUhTM0k2Mk1SMlJLR1U0UU5NWUo2TzVNVVFPNEhEV0s0UERRQ1YyQlpaVTdNNVNFVURCVUNHNzdTWkFSVUdPRTNVWlVINUlXWFlINVdMTEUyVzZTUDdXTkgzM0hQVVVSRTdNTjNQTEo3WFFJSlVTRFJCMlhJUUVCTFkzWVlGWFVONU5DSzVZRTVaUVQ0WU41NFNaVFZKVVZLRzRPUSJ9fQ.ZoqEZKOO2QWXN5R02ldaWf_oLFWTFg6mEu6pLaaQsyBrxtC7fVFTcmL30cMPfXvffEWyfORNTusZmDa9RVIb85QJ7b3l7ufRgO-luS3aPYv5KKt-V8ac0ns0LVkLnBbAescdfQeStl796Q1eMAq0ukVUWCgHIIkUHdFxnshfMZVN9WmbnAFn8PuLkrKdWMC8F7XRFRciZR4DkkCzdf3VwBpg-qN1HXMbJRZQ33J2qs6kTfk_bI3SGc_2LF3e6NTXoZz136r8H-doNFTU2oalVTtcPOgzJjuLiqvOX8orjPAMUKi4_JyxVS6ZwjOqYqtr81AIepgXSbaQwytSGCz7xg" } }, "request": { "type": "LaunchRequest", "requestId": "amzn1.echo-api.request.087b167f-722d-472f-b528-415e82441085", "timestamp": "2018-09-17T13:52:51Z", "locale": "en-US", "shouldLinkResultBeReturned": false } }

The Azure Function returns a 500 however if i manually change the request tag to be

"LaunchRequest": {
    "type": "LaunchRequest",
    "requestId": "amzn1.echo-api.request.087b167f-722d-472f-b528-415e82441085",
    "timestamp": "2018-09-17T13:52:51Z",
    "locale": "en-US",
    "shouldLinkResultBeReturned": false
}

The Azure Function returns a 200.

stoiveyp commented 6 years ago

Hi @damienmccauley - the request type is the discriminating value that determines what fields should be available with each request, which is why request remains abstract.

I also got an error trying to re-run my old sample; so this still appears to be an issue with Azure Function bindings not honoring Json.Net custom converters. I've made a small update to the sample function to show that this is the case by manually deserializing the request inside the function - which should output json as expected.

 [FunctionName("AlexaNetSample")]
        public static IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]string req, TraceWriter log)
        {
            log.Info("C# HTTP trigger function processed a request.");

            var request = JsonConvert.DeserializeObject<SkillRequest>(req);

            if (request.Request is LaunchRequest)
            {
                return new OkObjectResult(ResponseBuilder.Tell("you launched from method param"));
            }

            return new OkObjectResult(ResponseBuilder.Empty());
        }

Hope this is a suitable workaround

damienmccauley commented 6 years ago

Thanks @stoiveyp, right you are on the AzureFuns bindings.

mikelor commented 6 years ago

Hi All, There was a change in the HttpTrigger signature from version 1.0 to 2.0 of Azure Functions. The HttpRequest req parameter became HttpRequestMessage req

To work around this, I did the following

SkillRequest input = await req.Content.ReadAsAsync<SkillRequest>();

Here's a loose sample...

      [FunctionName("AlexaSkill)]
        public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            // Get to the content
            SkillRequest input = await req.Content.ReadAsAsync<SkillRequest>();

            string responseMessage = "";

            switch (input.Request)
            {
                case LaunchRequest launch:
                    responseMessage = @"Welcome to the Machine";
                    break;
                case IntentRequest intent:
                    switch (intent.Intent.Name)
                    {
                        case BuiltInIntent.Cancel:
                        case BuiltInIntent.Stop:
                            //blah blah
                            break;
                        default:
                            break;
                    }
                    break;
                default:
                    break;
            }
            return new OkObjectResult(ResponseBuilder.Tell(responseMessage));

        }
jaddie commented 5 years ago

This one can be closed too? :)