Attributes In Practice - Building A Payment Terminal

Attributes act as markers in the .NET platform. It is with their use that it is possible to write aspect-oriented business processes. Attributes are one of the most successful practices of Aspect Oriented Programming.

In this article and the next few articles, we will try to describe Attributes practically. Attributes write additional information to the metadata. By reading those data with reflection, it is possible to form logic in the program, thereby performing several operations. One thing to remember about attributes is that it works with reflection. Validation, authorization, authentication, role system, the definition of contracts, etc. on the .NET platform. such processes can be done through attributes. In one of our previous lessons, we developed a dynamic provider system by applying interface and reflection (link here). We'll rewrite that system using attributes instead of interfaces.

The description of our technical task is as follows: We need to develop a payment terminal for our company. The basic code structure of that payment terminal is already written. As new providers are added, we need to be able to dynamically add providers to the application without making any changes to the underlying code structure. That is, when a new provider is written, we should not enter and change the code of the main program. Each of the providers will be in .dll format and will be placed in a separate folder (the libs folder in our example) after being written. The program should read these DLLs from that folder and build a provider corresponding to each DLL on the screen.

First, we create a new solution and create 4 projects there. 1 of the project will be WinForm(MainApplicationUI), and the other 3 will be DLLs(class library).

(You can download the project from here.)

Attributes in Practice : Building a payment terminal

All application logic will be centralized in the ProviderProtocol.DLL. All other projects must keep a reference to that DLL. Here, the main application (MainApplicationUI) and providers (AzercellProject, MegaSigortaProject) communication contract is given through attributes.

The ProviderProtocol DLL holds 2 attributes. With ProviderNameAttribute we will specify the name of each provider, while ProviderOperationAttribute stores the event that will occur in the graphical interface and its implementation.

Attributes in Practice : Building a payment terminal

By writing the ProviderNameAttribute to each provider, we specify the name under which that provider will appear in the UI.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ProviderProtocol {
    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
    public sealed class ProviderNameAttribute: Attribute {
        public string ProviderName {
            get;
            private set;
        }
        public ProviderNameAttribute(string providerName) {
            ProviderName = providerName;
        }
    }
}

With ProviderOperationAttribute, we show what action will happen when each provider is clicked or double-clicked. We have also created a separate enum for the type of operation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ProviderProtocol {
    public enum EventType {
        Click,
        DblClick,
        MouseMove
    }
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    public sealed class ProviderOperationAttribute: Attribute {
        public EventType EventType {
            get;
            private set;
        }
        // This is a positional argument
        public ProviderOperationAttribute(EventType eventType) {
            EventType = eventType;
        }
    }
}

The code inside AzercellProject is very simple. Here we have just marked our class with the necessary attributes.

using ProviderProtocol;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AzercellProject {
    [ProviderName("Azercell")]
    public class AzercellProvider {
        [ProviderOperation(EventType.Click)]
        public void OnButtonClicked() {
            //implementation simplified for learning..
            MessageBox.Show("This is Azercell provider");
        }
    }
}

The Mega Insurance project is also in the same form. The goal is simply to emulate the operation. Once the template is ready, you can write more elaborate business processes.

using ProviderProtocol;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AzercellProject {
    [ProviderName("MegaSigorta")]
    public class MegaSigortaProvider {
        [ProviderOperation(EventType.Click)]
        public void OnButtonClicked() {
            //implementation simplified for learning..
            MessageBox.Show("This is Mega Sigorta provider");
        }
    }
}

After our DLLs are ready, we build the general project and drop the received DLLs into the libs folder in the MainApplicationUI project.

And MainApplicationUI reads all the DLLs in the libs folder and applies reflection to them. It Checks if the classes and their methods in those DLLs implement the ProviderNameAttribute and ProviderOperationAttribute attributes. If the class implements the ProviderNameAttribute attribute, it creates a button on the interface with that name. If any method of the class has implemented the ProviderOperationAttribute attribute, then it binds that method to the corresponding event and activates it.

namespace MainApplicationUI {
    public partial class MainForm: Form {
        ProviderVisualizer _providerVisualizer;
        public MainForm() {
            InitializeComponent();
            _providerVisualizer = new ProviderVisualizer(grbx_providers);
        }
        private void MainForm_Load(object sender, EventArgs e) {
            //get path to libs folder
            string libsPath = ApplicationPath.PathTo("libs");
            _providerVisualizer.LoadFrom(libsPath);
        }
    }
}

As you can see, the main processes are concentrated in the ProviderVisualizer class. Let's look at the internal code of that class. The main job of ProviderVisualizer is to find DLLs and add them to the system in a provider way.

namespace MainApplicationUI.Core {
    public class ProviderVisualizer {
        private readonly Control _control;
        private int _locationX;
        private int _locationY;
        public ProviderVisualizer(Control control) {
            _control = control;
            InitializeDefaultParams();
        }
        public void ClearProviders() {
            _control.Controls.Clear();
            InitializeDefaultParams();
        }
        private void InitializeDefaultParams() {
            _locationX = 20;
            _locationY = 34;
        }
        public void AddProvider(string providerName, Action operation, EventType eventType) {
            Button button = new Button {
                Text = providerName,
                    Size = new Size(150, 100),
                    Location = new Point(_locationX, _locationY)
            };
            if (eventType == EventType.Click) {
                button.Click += (sndr, args) => {
                    operation();
                };
            } else
            if (eventType == EventType.DblClick) {
                button.DoubleClick += (sndr, args) => {
                    operation();
                };
            }
            _locationX += 150;
            _control.Controls.Add(button);
        }
        public void LoadFrom(string path) {
            //get path to libs folder
            string libsPath = path;
            //get only DLL files
            string[] providers = Directory.GetFiles(libsPath, "*.dll");
            //for simplicity excaped LINQ query...
            //for every provider ....
            foreach(string provider in providers) {
                //load it into application RAM..
                Assembly assembly = Assembly.LoadFile(provider);
                //get all types in assembly
                Type[] assemblyTypes = assembly.GetTypes();
                foreach(Type assemblyType in assemblyTypes) {
                    //get provider name
                    ProviderNameAttribute providerNameAttr = (ProviderNameAttribute) assemblyType.GetCustomAttribute(typeof(ProviderNameAttribute));
                    // get the method 
                    var method = assemblyType.GetMethods().Where(x => x.GetCustomAttribute(typeof(ProviderOperationAttribute)) != null).FirstOrDefault();
                    //if provider name and method exist
                    if (providerNameAttr != null && method != null) {
                        //create object of the class
                        object instance = Activator.CreateInstance(assemblyType);
                        //provider operation for eventType
                        var providerOperationAttr = method.GetCustomAttribute < ProviderOperationAttribute > ();
                        this.AddProvider(providerNameAttr.ProviderName, () => {
                            method.Invoke(instance, null);
                        }, providerOperationAttr.EventType);
                    }
                }
            }
        }
    }
}

Now, after building the project, we drop both providers into the libs folder of the main app and when we run the app, we see both providers appear in the UI.

Attributes in Practice : Building a payment terminal

You can download the project from here.


Similar Articles