0
\$\begingroup\$

So this is an automatic email sending application, it reads with Handlebar the email templates and assembles them, and then sends the email classes.

My goal with the code is to make it as easy to unit-test as possible, I haven't added any unit tests yet, but I plan to, I just need to learn a bit more unit-testing with simpler things.

What can be improved with the code? Should I use generic, create a email Collection class, a way to change the smtp settings in .config? Any tips is helpful, but please keep it as simple as possible. As I'm still very much new to programming. Maybe I shouldn´t use the temp smtp server like this, which will make it harder to setup a real one?

The code has been tested by setting up a temporary smtp server at mailtrap.io, and all the email seems to be arriving properly.

Mail sending classes

  sealed class NullMailSender : IMailSender
        {
            public void Send(IList<string> error, string from, string to, string title, string body)
            {

            }
        }

        sealed class SmtpMailSender : IMailSender
        {
            private const int NumberOfRetriesOnError = 3;
            private const int DelayOnError = 1000;

            public void Send(IList<string> errors, string from, string to, string title, string body)
            {
                for (int i = 0; i <= NumberOfRetriesOnError; ++i)
                {
                    try
                    {
                        using (var x = new SmtpClient("smtp.mailtrap.io", 2525))
                        {
                            var smtp = new SmtpClient("smtp.mailtrap.io", 2525)
                            {
                                Credentials = new NetworkCredential("username", "password"),
                                EnableSsl = true
                            };
                            var mail = new MailMessage();
                            mail.From = new MailAddress(from);
                            mail.Subject = title;
                            mail.To.Add(to);
                            mail.Body = body;

                            smtp.Send(mail);
                            Console.WriteLine("Sending {0} mail to:{1}...", title, to);
                        }

                        break;
                    }
                    catch (SmtpException e)
                    {
                        if (i < NumberOfRetriesOnError)
                            Thread.Sleep((i + 1) * DelayOnError);
                        else
                            errors.Add($"{to}: {e.Message}");
                    }
                }
            }
        }
    }

The mail assembly, collecting and figuring out what is supposed to be in the email.

public class MailAssembly
    {
        public List<string> Errors { get; } = new List<string>();

        public IEnumerable<Customer> Customers { get; set; }

        public IEnumerable<Order> Orders { get; set; }

        public IMailSender Sender { get; set; }

        private const string OurEmailAddress = "[email protected]";

        //Sends all new customers, with the title of the emails and the email template to the send function
        public void SendWelcomeEmails()
        {
            Send(GetNewCustomers(), "Welcome", Resources.WelcomeEmailTemplate);
        }
        //Sends all customer without recent orders, with the title "Comeback" with the email temple to the send function
        public void SendComeBackEmail()
        {
            Send(GetCustomersWithoutRecentOrders(), "ComeBack", Resources.ComeBackEmailTemplate);
        }
        // Returns all new customers in the "database" who are newer than 1 day
        private IEnumerable<Customer> GetNewCustomers()
        {
            var yesterday = DateTime.UtcNow.Date.AddDays(-1);
            return Customers.Where(x => x.CreatedDateTime >= yesterday);
        }
        // Returns all customers who haven't made an order in 1 month
        private IEnumerable<Customer> GetCustomersWithoutRecentOrders()
        {
            var oneMonthAgo = DateTime.UtcNow.Date.AddMonths(-1);

            return Customers.Where(c =>
            {
                var latestOrder = Orders
                    .Where(o => o.CustomerEmail == c.Email)
                    .OrderByDescending(o => o.OrderDatetime)
                    .FirstOrDefault();

                return latestOrder != null
                    && latestOrder.OrderDatetime < oneMonthAgo;
            });
        }
        // The Handlebar reads the email temlate, then sends all the email data to the interface send function
        private void Send(IEnumerable<Customer> customers, string title, string template)
        {
            if (Sender == null)
                throw new InvalidOperationException("Cannot send...");

            Errors.Clear();
            foreach (var customer in customers)
            {
                var compiledTemplate =
                HandlebarsDotNet.Handlebars.Compile(template);
                var body = compiledTemplate(new { CompanyName = "Company", FullName = customer.FullName, valueOfVoucher = "50", voucher = "{voucherAcessCode}" });
                Sender.Send(Errors, OurEmailAddress, customer.Email, title, body);
            }
        }
    }
}

Interface part

public interface IMailSender
    {
        void Send(IList<string> error, string from, string to, string title, string body);
    }

Main method, creating an instance of the classes

static void Main(string[] args)


     {
            var sender = new MailAssembly
            {
                Customers = DataLayer.ListCustomers(),
                Orders = DataLayer.ListOrders(),
                Sender = new SmtpMailSender()
            };
            sender.SendComeBackEmail();
            sender.SendWelcomeEmails();
            if (sender.Errors.Any()) Console.WriteLine(String.Join("\n", sender.Errors));
            else
                Console.WriteLine("All emails should have been sent succesfully");
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();

        }

And finally, the data layer

 public class Customer
    {
        public string FullName { get; set; }
        public string Email { get; set; }
        public DateTime CreatedDateTime { get; set; }
    }

    public class Order
    {
        public string CustomerEmail { get; set; }
        public DateTime OrderDatetime { get; set; }
    }

    class DataLayer
    {
        /// <summary>
        /// Mockup method for all customers
        /// </summary>
        public static List<Customer> ListCustomers()
        {
            return new List<Customer>()
                   {
                       new Customer(){Email = "[email protected]", FullName = "Jane Porter", CreatedDateTime = DateTime.UtcNow.AddHours(-7)},
                       new Customer(){Email = "[email protected]", FullName = "Jeremy Longbottom",CreatedDateTime = DateTime.UtcNow.AddDays(-1)},
                       new Customer(){Email = "[email protected]", FullName = "Jock Ginger",CreatedDateTime = DateTime.UtcNow.AddMonths(-6)},
                       new Customer(){Email = "[email protected]", FullName = "Jeryll Potter",CreatedDateTime = DateTime.UtcNow.AddMonths(-1)},
                       new Customer(){Email = "[email protected]", FullName = "Jack Jacksson",CreatedDateTime = DateTime.UtcNow.AddMonths(-2)},
                       new Customer(){Email = "[email protected]", FullName = "Dope Jake",CreatedDateTime = DateTime.UtcNow.AddDays(-5)}
                   };
        }

        /// <summary>
        /// Mockup method for listing all orders
        /// </summary>
        public static List<Order> ListOrders()
        {
            return new List<Order>()
                   {
                       new Order(){CustomerEmail = "[email protected]", OrderDatetime = DateTime.UtcNow.AddMonths(-6)},
                       new Order(){CustomerEmail = "[email protected]", OrderDatetime = DateTime.UtcNow.AddMonths(-2)},
                       new Order(){CustomerEmail = "[email protected]", OrderDatetime = DateTime.UtcNow.AddDays(-2)}
                   };
        }
\$\endgroup\$
1
  • \$\begingroup\$ In SmtpMailSender.Send() you should not have both x and smtp, merge them... \$\endgroup\$ Commented Aug 30, 2017 at 14:16

1 Answer 1

2
\$\begingroup\$

Upfront I see two issues.

1) Firs one is obvious and in SmtpMailSender class in Send method

using (var x = new SmtpClient("smtp.mailtrap.io", 2525))
{
    var smtp = new SmtpClient("smtp.mailtrap.io", 2525)
    {
        Credentials = new NetworkCredential("username", "password"),
        EnableSsl = true
    };
    var mail = new MailMessage();
    mail.From = new MailAddress(from);
    mail.Subject = title;
    mail.To.Add(to);
    mail.Body = body;

    smtp.Send(mail);
    Console.WriteLine("Sending {0} mail to:{1}...", title, to);
}

You need to know what the keyword using is responsible for. It creates an object that has to implement IDisposable interface and then it makes sure that even in case of the worst exception it will run the Dispose method from that interface.

What you do here is create an SmtpClient instance named x that is disposed when we exit the scope, and then you make a new instance of that object smtp and you never dispose it. Connection probably remains open until garbage collector removes it. This is a bug and might cause an exception. You should do something like this:

using (var smtp = new SmtpClient("smtp.mailtrap.io", 2525) {
        Credentials = new NetworkCredential("username", "password"),
        EnableSsl = true
    })
{
    var mail = new MailMessage();
    mail.From = new MailAddress(from);
    mail.Subject = title;
    mail.To.Add(to);
    mail.Body = body;

    smtp.Send(mail);
    Console.WriteLine("Sending {0} mail to:{1}...", title, to);
}

2) Second issue I see is in MailAssembly class in GetCustomersWithoutRecentOrders method

    private IEnumerable<Customer> GetCustomersWithoutRecentOrders()
    {
        var oneMonthAgo = DateTime.UtcNow.Date.AddMonths(-1);

        return Customers.Where(c =>
        {
            var latestOrder = Orders
                .Where(o => o.CustomerEmail == c.Email)
                .OrderByDescending(o => o.OrderDatetime)
                .FirstOrDefault();

            return latestOrder != null
                && latestOrder.OrderDatetime < oneMonthAgo;
        });
    }

The linq you wrote is not optimal. For every customer, you check if there is an order with hes email address then you order them by OrderDatetime and check the date of the first one.

You can optimize it like this:

    private IEnumerable<Customer> GetCustomersWithoutRecentOrders()
    {
        var oneMonthAgo = DateTime.UtcNow.Date.AddMonths(-1);

        return Customers.Where(c =>
        {
            return !Orders.Any(o => o.CustomerEmail == c.Email 
                                 && o.OrderDatetime >= oneMonthAgo).
        });
    }

This code every customer checks every order if the e-mail belongs to the customer, and if the date is fresh enough. As soon as it finds first one it returns false (due to negation !) and exits the loop. No need to do O(n*logn) sorting anymore.

I made those fixes without testing them so please treat them as pseudocode.

\$\endgroup\$
6
  • \$\begingroup\$ Your predicate for Where in LINQ query is not equivalent of the original one. Just check results for Orders where there are no elements with CustomerEmail == c.Email. Original predicate will return false but your predicate will return true. \$\endgroup\$
    – Maxim
    Commented Aug 31, 2017 at 6:38
  • \$\begingroup\$ @Maxim You are 100% correct! But when I read just now the function name I'm not 100% sure that this isn't a bug in the original code. JsonDork could you please analyze what do you want this function to return in this case of no orders per customer? \$\endgroup\$ Commented Aug 31, 2017 at 8:33
  • \$\begingroup\$ By the way, if you want to save the original logic, your predicate in Any should be if o.CustomerEmail == c.Email then o.OrderDatetime >= oneMonthAgo. It's called implication and can be expressed via negation and OR: A -> B the same as (NOT A) OR B. So fix is simple: o => o.CustomerEmail != c.Email || o.OrderDatetime >= oneMonthAgo. \$\endgroup\$
    – Maxim
    Commented Aug 31, 2017 at 12:11
  • \$\begingroup\$ Hey, sorry for late answer. If no customers have any orders, I would like them to return nothing, basically. Send none of this kind of email. \$\endgroup\$
    – JsonDork
    Commented Sep 1, 2017 at 9:31
  • \$\begingroup\$ In this case I would separate this method into three. GetCustomersForMailing, CustomerHasOrders, CustomerHasRecentOrder to make sure everyone knows what the procedure should do. I will update my answer when I'll return home from holidays in few days. \$\endgroup\$ Commented Sep 1, 2017 at 16:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.