axuno / SmartFormat

A lightweight text templating library written in C# which can be a drop-in replacement for string.Format
Other
1.1k stars 105 forks source link

Some values are missing when passing in an object #127

Closed perason closed 4 years ago

perason commented 4 years ago

Hi.

I am posting a form and resolve its fields to an object of type Bid (see classes at the end).

When I run the following code snippet:

Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid)

Output:
Navn: Per Bykom (Bykom)
Tlf/Epost: 123456 / per.andersson@email.no
Verk/Bud: BF-snartvårkjenning / 
Veldedig formål: 

At line 3 and 4 the {Value} and {Comment} values are missing.


When I use built-in string interpolation it works just fine:

$"Navn: {bid.Firstname} {bid.Lastname} ({bid.Company})\nTlf/Epost: {bid.Phone} / {bid.Email}\nVerk/Bud: {bid.ItemProductId} / {bid.Value}\nVeldedig formål: {bid.Comment}"

Output:
Navn: Per Bykom (Bykom)
Tlf/Epost: 123456 / per.andersson@email.no
Verk/Bud: BF-snartvårkjenning / 8000
Veldedig formål: Hej Nina. Testar att epostbekräftelse fungerar.

When serializing the bid object, one can see they both exists

{
  "Auction": "Skogen min",
  "ItemProductId": "BF-snartvårkjenning",
  "ItemId": 1250,
  "Date": "2020-04-03T22:44:36.9333205+02:00",
  "Value": "8000",
  "Comment": "Hej Nina. Testar att epostbekräftelse fungerar.",
  "AcceptTerms": true,
  "Firstname": "Per",
  "Lastname": "Bykom",
  "Company": "Bykom",
  "Phone": "123456",
  "Email": "per.andersson@email.no",
  "Id": 0,
  "CreateDate": "0001-01-01T00:00:00",
  "EditDate": "0001-01-01T00:00:00",
  "EditBy": null,
  "Owner": null,
  "Deleted": false,
  "Deactivated": false,
  "PublishedState": 0,
  "Published": false,
  "PublishSite": null,
  "PublishStartDate": "0001-01-01T00:00:00",
  "PublishStopDate": "0001-01-01T00:00:00",
  "Category": null,
  "SubCategory": null
}

My project is a ASP.NET Core 3.1 (v. 3.1.3/SDK 3.1.201) web application and this is how the classes looks like

public class MyModels
{
    public int Id { get; set; }
    public DateTime CreateDate { get; set; }
    public DateTime EditDate { get; set; }
    public string EditBy { get; set; }
    public string Owner { get; set; }

    public bool Deleted { get; set; }
    public bool Deactivated { get; set; }

    public int PublishedState { get; set; }
    public bool Published { get; set; }
    public string PublishSite { get; set; }
    public DateTime PublishStartDate { get; set; }
    public DateTime PublishStopDate { get; set; }

    public string Category { get; set; }
    public string SubCategory { get; set; }
}

public class Bid : MyModels
{
    public string Auction { get; set; }
    public string ItemProductId { get; set; }
    public int ItemId { get; set; }
    public DateTime Date { get; set; }

    public string Value { get; set; }
    public string Comment { get; set; }
    public bool AcceptTerms { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public string Company { get; set; }
    public string Phone { get; set; }
    public string Email { get; set; }
}
axunonb commented 4 years ago

Hi, It would be good to know which version of SmartFormat and which SmartSettings you are using. Assuming the latest v2.5.0 with default SmartSettings, your snippets compiled to a working sample do their job as expected. So I couldn't preproduce the described behavior. See the working sample below.

namespace Play
{
    /// <summary>
    /// Using DotNet Core 3.1 console application and SmartFormat 2.5.0
    /// </summary>
    public static class PerasonIssue127
    {
        public static void Test()
        {
            var bidJson = @"{
  'Auction': 'Skogen min',
  'ItemProductId': 'BF-snartvårkjenning',
  'ItemId': 1250,
  'Date': '2020-04-03T22:44:36.9333205+02:00',
  'Value': '8000',
  'Comment': 'Hej Nina. Testar att epostbekräftelse fungerar.',
  'AcceptTerms': true,
  'Firstname': 'Per',
  'Lastname': 'Bykom',
  'Company': 'Bykom',
  'Phone': '123456',
  'Email': 'per.andersson@email.no',
  'Id': 0,
  'CreateDate': '0001-01-01T00:00:00',
  'EditDate': '0001-01-01T00:00:00',
  'EditBy': null,
  'Owner': null,
  'Deleted': false,
  'Deactivated': false,
  'PublishedState': 0,
  'Published': false,
  'PublishSite': null,
  'PublishStartDate': '0001-01-01T00:00:00',
  'PublishStopDate': '0001-01-01T00:00:00',
  'Category': null,
  'SubCategory': null
}";

            var bid = JsonConvert.DeserializeObject<Bid>(bidJson);

            Console.WriteLine(SmartFormat.Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid));
            /*
             * Output as expected is:
Navn: Per Bykom (Bykom)
Tlf/Epost: 123456 / per.andersson@email.no
Verk/Bud: BF-snartvårkjenning / 8000
Veldedig formål: Hej Nina. Testar att epostbekräftelse fungerar.
             */
        }
    }

    public class MyModels
    {
        public int Id { get; set; }
        public DateTime CreateDate { get; set; }
        public DateTime EditDate { get; set; }
        public string EditBy { get; set; }
        public string Owner { get; set; }

        public bool Deleted { get; set; }
        public bool Deactivated { get; set; }

        public int PublishedState { get; set; }
        public bool Published { get; set; }
        public string PublishSite { get; set; }
        public DateTime PublishStartDate { get; set; }
        public DateTime PublishStopDate { get; set; }

        public string Category { get; set; }
        public string SubCategory { get; set; }
    }

    public class Bid : MyModels
    {
        public string Auction { get; set; }
        public string ItemProductId { get; set; }
        public int ItemId { get; set; }
        public DateTime Date { get; set; }

        public string Value { get; set; }
        public string Comment { get; set; }
        public bool AcceptTerms { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public string Company { get; set; }
        public string Phone { get; set; }
        public string Email { get; set; }
    }
}
perason commented 4 years ago

Hi, and thanks for reply.

I use the latest version, 2.5.0, and default settings.

One thing though, I wrapped it in a "fire-and-forget", where it emails the output, like this:

Task.Run(async () =>
{
    await Emailer.SendSimpleAsync(RequestSettings, AppSettings, HttpContext, "Auction",
        "Bud gitt",
        Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid)
        );
}).Forget();

And here's the Forget extension method:

public static void Forget(this Task task)
{
    if (!task.IsCompleted || task.IsFaulted)
    {
        _ = ForgetAwaited(task);
    }
    async static Task ForgetAwaited(Task task)
    {
        try
        {
            await task.ConfigureAwait(false);
        }
        catch
        {
            // Nothing to do here
        }
    }
}
axunonb commented 4 years ago

Running our sample async will not make a difference. Give it a try. My best guest for further investigations is to have a look at your catch block, which is discarding all exceptions. Are you sure there's no exception thrown?

perason commented 4 years ago

No, I added a logging to that catch and no exceptions is thrown.

I did some more testing and when not using Task.Run it works properly every time, so something appears to go on when it is pushed to another thread.

axunonb commented 4 years ago

Please run the following sync and async code on your machine in an dotnet core 3.1 console application and let us know whether you still experience the problem. As long as we can't reproduce, it's hard to help.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Play
{
    /// <summary>
    /// Using DotNet Core 3.1 console application and SmartFormat 2.5.0
    /// </summary>
    public static class PerasonIssue127
    {
        public async static Task Test()
        {
            var bidJson = @"{
  'Auction': 'Skogen min',
  'ItemProductId': 'BF-snartvårkjenning',
  'ItemId': 1250,
  'Date': '2020-04-03T22:44:36.9333205+02:00',
  'Value': '8000',
  'Comment': 'Hej Nina. Testar att epostbekräftelse fungerar.',
  'AcceptTerms': true,
  'Firstname': 'Per',
  'Lastname': 'Bykom',
  'Company': 'Bykom',
  'Phone': '123456',
  'Email': 'per.andersson@email.no',
  'Id': 0,
  'CreateDate': '0001-01-01T00:00:00',
  'EditDate': '0001-01-01T00:00:00',
  'EditBy': null,
  'Owner': null,
  'Deleted': false,
  'Deactivated': false,
  'PublishedState': 0,
  'Published': false,
  'PublishSite': null,
  'PublishStartDate': '0001-01-01T00:00:00',
  'PublishStopDate': '0001-01-01T00:00:00',
  'Category': null,
  'SubCategory': null
}";

            var bid = JsonConvert.DeserializeObject<Bid>(bidJson);
            Console.WriteLine("SYNC:\n");
            Console.WriteLine(SmartFormat.Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid));
            /*
             * Output:
Navn: Per Bykom (Bykom)
Tlf/Epost: 123456 / per.andersson@email.no
Verk/Bud: BF-snartvårkjenning / 8000
Veldedig formål: Hej Nina. Testar att epostbekräftelse fungerar.
             */
             Console.WriteLine("ASYNC:\n");
            var result = await Task.Run(async () =>
            {
                var bid2 = JsonConvert.DeserializeObject<Bid>(bidJson);
                await Task.Delay(1000).ConfigureAwait(false);
                return SmartFormat.Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid2);

            }).ConfigureAwait(false);
            Console.WriteLine(result);
            /*
             * Output:
Navn: Per Bykom (Bykom)
Tlf/Epost: 123456 / per.andersson@email.no
Verk/Bud: BF-snartvårkjenning / 8000
Veldedig formål: Hej Nina. Testar att epostbekräftelse fungerar.
             */
        }
    }

    public class MyModels
    {
        public int Id { get; set; }
        public DateTime CreateDate { get; set; }
        public DateTime EditDate { get; set; }
        public string EditBy { get; set; }
        public string Owner { get; set; }

        public bool Deleted { get; set; }
        public bool Deactivated { get; set; }

        public int PublishedState { get; set; }
        public bool Published { get; set; }
        public string PublishSite { get; set; }
        public DateTime PublishStartDate { get; set; }
        public DateTime PublishStopDate { get; set; }

        public string Category { get; set; }
        public string SubCategory { get; set; }
    }

    public class Bid : MyModels
    {
        public string Auction { get; set; }
        public string ItemProductId { get; set; }
        public int ItemId { get; set; }
        public DateTime Date { get; set; }

        public string Value { get; set; }
        public string Comment { get; set; }
        public bool AcceptTerms { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public string Company { get; set; }
        public string Phone { get; set; }
        public string Email { get; set; }
    }
}
axunonb commented 4 years ago

Could you verify the result of the code above?

perason commented 4 years ago

The above code works, just as mine does, when not using Task.Run.

The big difference is how we call Task.Run, where I don't await mine. I want mine to be a "fire-and-forget", so I don't have to wait the email method to finish.

After some further tests, I got mine to fail for the built-in "string interpolation" as well, but not when storing the result in a variable prior to calling Task:Run (using either Smart.Format or the built-in) and then use the variable inside the Task.Run block.

So I guess that when my Task.Run finally runs, the bid object partially lost some of its property values.

For now I use the workaround adding the formatted result to a variable, and when I get som time I will test your way, using an awaited Task.Run.

axunonb commented 4 years ago

Okay, now it's clear. Your Forget method might not be finished when the calling task will terminate. I could simulate this in a console application.

Task.Run(async () =>
{
    await Task.Delay(20).ConfigureAwait(false); // send email
    var bid = JsonConvert.DeserializeObject<Bid>(bidJson);
    var result = SmartFormat.Smart.Format("Navn: {Firstname} {Lastname} {Company:({Company})|}\nTlf/Epost: {Phone} / {Email}\nVerk/Bud: {ItemProductId} / {Value}\nVeldedig formål: {Comment}", bid);
    Console.WriteLine("FROM FORGET");
    Console.WriteLine(result);
}).Forget();

If there is a Task.Delay(x) after calling the Forget Method, the output is complete, when x > 300 (on my machine). If it is shorter, all or part of the output will be missing. I'm afraid this is all I can contribute here.

axunonb commented 4 years ago

Is there something new to reproduce the described behavior or should we close the issue?

perason commented 4 years ago

Given that I now know the issue has nothing to do with SmartFormat, please feel free to delete this complete post.

And again, thanks a lot for both your help and a great library.