Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A 'To' address must be specified for the message using SendGridOutput #737

Closed
AsierVillanueva opened this issue Dec 1, 2021 · 11 comments
Closed
Assignees

Comments

@AsierVillanueva
Copy link

When I try to send a email with SendGrid binding the Azure Function output with SendGridOutputAttribute then function fail with this error: A 'To' address must be specified for the message.

I use .NET 5.0 dotnet-isolated runtime.

Sample code:

    public static class CheckSendGrid
    {
        [Function("CheckSendGridOutput")]
        [SendGridOutput(ApiKey = "SendGridApiKey")]
        public static SendGridMessage Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req,
                FunctionContext executionContext)
        {
            var logger = executionContext.GetLogger("HttpFunction");

            var message = new SendGridMessage();
            message.SetFrom(new EmailAddress("foo@foo.com", "Sender"));
            message.SetGlobalSubject("Check SendGrid output");
            message.AddTo(new EmailAddress("foo@foo.com", "To Address"));
            message.AddContent(MimeType.Text, "Mail to check SendGrid output from Azure Function");

            logger.LogInformation($"Send message {message.Subject} to {message.Personalizations.First().Tos.First().Email}");

            return message;
        }
    }

When I call the function the response is:

image

@ghost ghost assigned satvu Dec 1, 2021
@kshyju kshyju assigned kshyju and unassigned satvu Dec 2, 2021
@kshyju
Copy link
Member

kshyju commented Dec 2, 2021

Thanks @AsierVillanueva for reporting the issue.

From the initial investigation, it looks like this is caused by the conflicting JSON serializers being used (SendGrid Personalization type uses JSON.NET while the isolated worker uses System.Text.JSON. The JsonProperty attribute(from JSON.NET) is not being respected while serializing, thus producing incorrect JSON. STJ uses JsonPropertyName to specify the property name.

Relevant part of the (incorrect) JSON produced (and sent form worker to host)

  "Subject": "Check SendGrid output",
  "Personalizations": [
    {
      "Tos": [
        {
          "Name": "To Address",
          "Email": "foo@bar.com"
        }
      ],

Ideally, it should be To instead of Tos in the JSON string.

Just to confirm from your side as well, do you still see the issue when you use newtonsoft JSON.NET serializer? Refer this sample bootstrapping code to configure JSON.NET as serializer. (You need to add a reference to the Microsoft.Azure.Core.NewtonsoftJson package to use the NewtonsoftJsonObjectSerializer type.)

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =>
    {
        workerApplication.UseNewtonsoftJson();
    })
    .Build();

cc @brettsam @fabiocav

@AsierVillanueva
Copy link
Author

Just to confirm from your side as well, do you still see the issue when you use newtonsoft JSON.NET serializer? Refer this sample bootstrapping code to configure JSON.NET as serializer. (You need to add a reference to the Microsoft.Azure.Core.NewtonsoftJson package to use the NewtonsoftJsonObjectSerializer type.)

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(workerApplication =>
    {
        workerApplication.UseNewtonsoftJson();
    })
    .Build();

Confirmed, with JSON.NET as serializer works fine.

@AsierVillanueva
Copy link
Author

As a workaround, it would be possible to set a serializer only for a specific function?

@kshyju
Copy link
Member

kshyju commented Dec 3, 2021

No, it is not possible to set a serializer only for a specific function. The serializer you set on the bootstrapping code (inside ConfigureFunctionsWorkerDefaults method) will be applied to all functions within the app.

@kshyju
Copy link
Member

kshyju commented Dec 3, 2021

@AsierVillanueva We will discuss this item in the next triage meeting and will share an update.

Until we have a proper official fix/guidance available, one 🐎 hacky solution to mitigate the issue is to create your own local copy of the SendGridMessage type and use JsonPropertyName attribute from STJ to customize the property names in the JSON string and use that.

Here is a minimal version of the type definition I used for validating.

public class MySendGridMessageSlim
{
    public EmailAddressSlim From { get; set; }
    public string Subject { get; set; }
    public List<PersonalizationSlim> Personalizations { get; set; }
    [System.Text.Json.Serialization.JsonPropertyName("Content")]
    public List<ContentSlim> Contents { get; set; }
}

public class EmailAddressSlim
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class PersonalizationSlim
{
    [System.Text.Json.Serialization.JsonPropertyName("to")]
    public List<EmailAddressSlim> Tos { get; set; }
}

public class ContentSlim
{
    public string Type { get; set; }
    public string Value { get; set; }
}

and your function code will use it like this.

[SendGridOutput(ApiKey = "SendGridApiKey")]
public static MySendGridMessageSlim Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    FunctionContext executionContext)
{
    var logger = executionContext.GetLogger("Function1");

    var message = new MySendGridMessageSlim
    {
        From = new EmailAddressSlim { Name = "Sender", Email = "foo@bar.com" },
        Subject = "Check SendGrid output with my DTO",
        Personalizations = new List<PersonalizationSlim>()
        {
            new PersonalizationSlim
            {
                Tos = new List<EmailAddressSlim>
                {
                    new EmailAddressSlim { Name ="Receiver", Email="bar@foo.com"}
                }
            }
        },
        Contents = new List<ContentSlim>()
        {
            new ContentSlim { Type ="text/plain", Value="This temp solution works." }
        }
    };

    logger.LogInformation($"Send message {message.Subject} to {message.Personalizations.First().Tos.First().Email}");

    return message;
}

@AsierVillanueva
Copy link
Author

Ok. My types for workaround:

using SendGrid.Helpers.Mail;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

public class FixJsonSendGridMessage
{
    public EmailAddress From { get; set; }

    public string Subject { get; set; }

    public List<FixJsonPersonalization> Personalizations { get; set; }

    public string PlainTextContent { get; set; }

    public string HtmlContent { get; set; }

    [JsonPropertyName("content")]
    public List<Content> Contents { get; set; }

    public List<Attachment> Attachments { get; set; }

    public string TemplateId { get; set; }

    public Dictionary<string, string> Headers { get; set; }

    public Dictionary<string, string> Sections { get; set; }

    public List<string> Categories { get; set; }

    public Dictionary<string, string> CustomArgs { get; set; }

    public long? SendAt { get; set; }

    public ASM Asm { get; set; }

    public string BatchId { get; set; }

    public string IpPoolName { get; set; }

    public MailSettings MailSettings { get; set; }

    public TrackingSettings TrackingSettings { get; set; }

    public EmailAddress ReplyTo { get; set; }


    public static implicit operator FixJsonSendGridMessage(SendGridMessage sendGridMessage) =>
        new()
        {
            Asm = sendGridMessage.Asm,
            Attachments = sendGridMessage.Attachments,
            BatchId = sendGridMessage.BatchId,
            Categories = sendGridMessage.Categories,
            Contents = sendGridMessage.Contents,
            CustomArgs = sendGridMessage.CustomArgs,
            From = sendGridMessage.From,
            Headers = sendGridMessage.Headers,
            HtmlContent = sendGridMessage.HtmlContent,
            IpPoolName = sendGridMessage.IpPoolName,
            MailSettings = sendGridMessage.MailSettings,
            Personalizations = sendGridMessage.Personalizations.Select(p => (FixJsonPersonalization)p).ToList(),
            PlainTextContent = sendGridMessage.PlainTextContent,
            ReplyTo = sendGridMessage.ReplyTo,
            Sections = sendGridMessage.Sections,
            SendAt = sendGridMessage.SendAt,
            Subject = sendGridMessage.Subject,
            TemplateId = sendGridMessage.TemplateId,
            TrackingSettings = sendGridMessage.TrackingSettings
        };

}

public class FixJsonPersonalization
{
    [JsonPropertyName("to")]
    public List<EmailAddress> Tos { get; set; }

    public List<EmailAddress> Ccs { get; set; }

    public List<EmailAddress> Bccs { get; set; }

    public EmailAddress From { get; set; }

    public string Subject { get; set; }

    public Dictionary<string, string> Headers { get; set; }

    public Dictionary<string, string> Substitutions { get; set; }

    public Dictionary<string, string> CustomArgs { get; set; }

    public long? SendAt { get; set; }

    public object TemplateData { get; set; }

    public static implicit operator FixJsonPersonalization(Personalization personalization) =>
        new()
        {
            Bccs = personalization.Bccs,
            Ccs = personalization.Ccs,
            CustomArgs = personalization.CustomArgs,
            From = personalization.From,
            Headers = personalization.Headers,
            SendAt = personalization.SendAt,
            Subject = personalization.Subject,
            Substitutions = personalization.Substitutions,
            TemplateData = personalization.TemplateData,
            Tos = personalization.Tos
        };
}

So the function code is almost the same:

    public static class CheckSendGrid
    {
        [Function("CheckSendGridOutput")]
        [SendGridOutput(ApiKey = "SendGridApiKey")]
        public static FixJsonSendGridMessage Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req,
                FunctionContext executionContext)
        {
            var logger = executionContext.GetLogger("HttpFunction");

            var message = new SendGridMessage();
            message.SetFrom(new EmailAddress("foo@foo.com", "Sender"));
            message.SetGlobalSubject("Check SendGrid output");
            message.AddTo(new EmailAddress("foo@foo.com", "To Address"));
            message.AddContent(MimeType.Text, "Mail to check SendGrid output from Azure Function");

            logger.LogInformation($"Send message {message.Subject} to {message.Personalizations.First().Tos.First().Email}");

            return (FixJsonSendGridMessage)message;
        }
    }

@kshyju
Copy link
Member

kshyju commented Dec 11, 2021

@AsierVillanueva Another solution is to explicitly generate the JSON string of your SendGridMessage object and return that from the function. You can use Newtonsoft JSON.NET serializer (which is what the SendGridMessage type also relies on) to produce the JSON string and return that string from your function. With this approach, the function infrastructure will not create a serialized string again (using System.Text.Json).

Here is a working sample. With this approach you do not need to duplicate the types locally. You can use the types coming from the SendGrid package.

[Function("Function1")]
[SendGridOutput(ApiKey = "SendGridApiKey")]
public static string Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    FunctionContext executionContext)
{
    var logger = executionContext.GetLogger("Function1");

    var message = new SendGridMessage();
    message.SetFrom(new EmailAddress("foo@bar.com", "Sender name"));
    message.SetGlobalSubject("Check SendGrid output");
    message.AddTo(new EmailAddress("bar@foo.com", "Receiver name"));
    message.AddContent(MimeType.Text, "SendGrid output from Azure Function with String return type");

    logger.LogInformation($"Send message {message.Subject} to {message.Personalizations.First().Tos.First().Email}");

    // Use Newtonsoft JSON serializer to produce JSON string. 
    var messageJson = Newtonsoft.Json.JsonConvert.SerializeObject(message);
            
    return messageJson;
}

Hope that helps. Let us know if you run into any issues.

@AsierVillanueva
Copy link
Author

Same problem with ContentId property of attachments

@fabiocav
Copy link
Member

fabiocav commented Jan 4, 2022

Based on the investigation done by @kshyju, a custom converter for this extension would be appropriate. Leaving this issue open to track that work and follow up on the SendGrid repo to see if they can also make enhancements to support System.Text.Json

@fabiocav
Copy link
Member

fabiocav commented Jan 4, 2022

Link to the issue they're using to track the move to STJ: sendgrid/sendgrid-csharp#985

@fabiocav fabiocav added this to the Functions Sprint 116 milestone Jan 12, 2022
@kshyju
Copy link
Member

kshyju commented Jan 20, 2022

@AsierVillanueva Another solution is to write some custom System.Text.Json JsonConverter implementations and hook it up with the function application during bootstrapping. S.T.J serializer will use these converters to customize the serialization result and you should be able to generate a JSON String which is similar to the JSON string produced by JSON.NET.

Here is a minimal working sample which uses some custom STJ converters to solve this issue. Hope this helps.

There is an open issue in the sendgrid repo to support STJ use case. Once they publish a new version of the library, we will publish a new version of the sendgrid extension to reflect that.

Closing this issue as there are various solutions to solve the problem at this point.

@kshyju kshyju closed this as completed Jan 20, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Feb 19, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants