A few weeks ago my boss gave me a task to improve our mail sending system. At that moment we had a system which would send email without parallel, single message after message. We don't send a lot of emails per day, but we also have irregular behavior. For example, the system was idle for two hours and after that it needed to send 100 emails. After consideration I have created a simple, message-based system based on SQL Server. It should work as if queue persisted in SQL Server single table. The email system isn't so big, so we didn't want to use RabbitMQ or MSMQ. In SQL Server we can create reports and audit data practically out of the box. Today I will present to you a smaller and not so complete version, but it's good enought to explain the assumptions. So le's switch to code.
Assumptions
Assumptions
- system should be based on SQL Server and single table .
- many parts of system can insert data to table (it represents sending emails) at the same time.
- system should process parallel data from one table - send many emails at the same time.
- if sending message has error, the system shouldn't mark (remove) message from table.
1. Database
The database code is very simple - all stuff is in one table.
The database code is very simple - all stuff is in one table.
- CREATE TABLE [dbo].[messages](
- [id] [int] IDENTITY(1,1) NOT NULL,
- [inserted] [datetime2](7) NOT NULL,
- [messageType] [nvarchar](512) NOT NULL,
- [messageBody] [nvarchar](max) NOT NULL,
- CONSTRAINT [PK_messages] PRIMARY KEY CLUSTERED
- (
- [id] ASC
- )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
- ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
- GO
- ALTER TABLE [dbo].[messages] ADD CONSTRAINT [DF_messages_inserted] DEFAULT (sysdatetime()) FOR [inserted]
- GO
- id - is unique message identifier - autoincrement,
- inserted - represent date, when message was inserted
- messageType - represent full C# type name (namespace + class name),
- messageBody - has serialized message.
2. C# Abstractions: I'd like to start programming from defining my interfaces. So let's do this.
IMessageProcessor: Implementation of this class will have logic for processing message -> in my work example, it send emails by SMTP.
- public interface IMessageProcessor<T>
- {
- void Process(T message);
- }
- public interface IMessageProcessorEngine
- {
- void ProcessAllMessages(int maxDegreeOfParallelism);
- }
- public interface IMessageRepository : IDisposable
- {
- void BeginTransaction();
- void CommitTransaction();
- void RollbackTransaction();
- DbMessageModel GetOldestMessage();
- void RemoveMessage(DbMessageModel message);
- }
- IMessageEngine - has logic to manage tasks.
- IMessageRepository - it responsible of manage a single message (row) in [dbo].[messages] sql table.
3. C# Implementation: Let's create class which represents the object structure of [dbo].[message] table row.
Next let's implements IMessageRepository,
- public class DbMessageModel
- {
- public int Id { get; set; }
- public DateTime Inserted { get; set; }
- public string MessageType { get; set; }
- public string MessageBody { get; set; }
- }
- public class MessageRepository : IMessageRepository
- {
- private SqlConnection _connection;
- private SqlTransaction _transaction;
- public void BeginTransaction()
- {
- _connection =
- new SqlConnection(ConfigurationManager.ConnectionStrings["ParallelMessageProcessingDb"].ConnectionString);
- _connection.Open();
- _transaction = _connection.BeginTransaction();
- }
- public void CommitTransaction()
- {
- _transaction.Commit();
- }
- public void RollbackTransaction()
- {
- _transaction.Rollback();
- }
- public DbMessageModel GetOldestMessage()
- {
- const string queryText = @"SELECT TOP (1)
- id,
- inserted,
- messageType,
- messageBody
- FROM dbo.messages WITH (ROWLOCK, READPAST, UPDLOCK, INDEX (PK_messages))
- ORDER BY id";
- using (var command = new SqlCommand(queryText, _connection, _transaction))
- {
- var dataTable = new DataTable();
- dataTable.Load(command.ExecuteReader());
- if (dataTable.Rows.Count == 0)
- return null;
- return new DbMessageModel
- {
- Id = (int) dataTable.Rows[0]["id"],
- Inserted = (DateTime) dataTable.Rows[0]["inserted"],
- MessageType = (string) dataTable.Rows[0]["messageType"],
- MessageBody = (string) dataTable.Rows[0]["messageBody"]
- };
- }
- }
- public void RemoveMessage(DbMessageModel message)
- {
- const string queryText = @"DELETE top(1) FROM dbo.messages WITH (ROWLOCK) WHERE id = @id";
- using (var command = new SqlCommand(queryText, _connection, _transaction))
- {
- var idParameter = new SqlParameter("id", SqlDbType.Int);
- idParameter.Value = message.Id;
- command.Parameters.Add(idParameter);
- command.ExecuteNonQuery();
- }
- }
- public void Dispose()
- {
- _transaction.Dispose();
- _connection.Dispose();
- }
- }

Vivek KumarPosted Apr 28, 2016, 2:58 PM
Good one
Vignesh ManiPosted Apr 14, 2016, 8:11 AM
Nice
Rahul Kumar SaxenaPosted Apr 14, 2016, 6:25 AM
Good Show
Debasis SahaPosted Apr 13, 2016, 1:43 PM
Nice one..
Mohammed IbrahimPosted Apr 13, 2016, 1:15 PM
nice
Pankaj Kumar ChoudharyPosted Apr 13, 2016, 12:27 PM
Great Explanation Sir. My Vote Is 5 for this article. Thanks for share such a nice information......