Skip to content

Services

Pavel Novikov edited this page Sep 28, 2020 · 12 revisions

The concept

Service contains application logic. Logic is a sequence of computations that produce number of To<Channel>.%something% calls that enqueue commands into commands queue for further dispatching. In the scope of Tecture, business logic makes decisions about which commands to run on external systems. Logic is the the code that takes user input, reads existing data from channels and computes series of commands to be executed on external systems.

Example: service responsible for Orders has business logic method that takes orderId and sends to DB SQL code that calls stored procedure performing clearing.

public class Orders : TectureService<Order>
    {
        private Orders() { }

        public void DoClearing(int orderId)
        {
            To<Db>().DirectSql(()=>$"sp_exec DoClearing {orderId}");
        }
    }

It is important to understand that sp_exec ... will not be executed immediately. .DirectSql does not address database immediately. It just enqueues the command that will send this command to database after you call ITecture.Save(). See integration section in order to understand where to trigger commands dispatching.

Defining service

As you might have noticed, services are classes that inherit TectureService<> class. There are two noticable things in Tecture service:

  • Private constructor. It is mandatory. Without it you will get runtime exception. Tecture ensures its services not to be instantiated via new or any other method. Warning! This mechanism may change soon in favor of using internal-only created class;
  • Service tooling. Service may contain up to 8 type parameters. There you can pass up to 8 toolings provided by various aspects. Toolings themselves can be parametrized in order to allow or restrict particular aspect capabilities within particular service. Proper usage of service tooling will be controlled in compile-time.

Aspects provide basic correctness checks supplying toolings for services. Aspects tend to be designed in a way to control your basic logic consistensy during compile time. Also they ensure predefined coding practices to be followed.

For example: ORM aspect has ability to add entities and, finally insert them into database. But it will not work until you specify addition tooling for service that uses this functionality. So aspect interfaces implemented by channel ensure that you will not insert entity into message queue or email (common system logic integrity). But service toolings ensure that you will not add Products from service that is working with Orders.

Consider this example:

//                                        |        T O O L I N G        |
public class SomeService : TectureService< Adds<Product>, Deletes<Order> >
{
	private SomeService() { }

	public void DoBusinessLogic()
	{
		// reading is possible from everywhere without restrictions
		var order = From<Db>().Get<Order>().ById(10);

		// that one is also possible but only because
		// we have specified in our service that it Adds<Product>
		To<Db>().Add(new Product() {Name = "New one"});

	
		// and here we will get compile-time error
		To<Db>().Add(new Order() {Name = "New one"});
					// ^-- will not compile because service must parametrize Adds<Product,Order> or Modifies<Product,Order>

		// and this is fine because service Deletes<Order>
		To<Db>().Delete(order);
	}
}

By creating your own aspects you can control strictness of such checks and balance between coding speed and safety.

The limitation of 8 toolings per service is intentional. It is presumed that if you make your service to carry more than 8 responsibilities - you are probably doing something wrong. Your service has to be de-coupled because it becomes difficult to understand.

Inheritance of Tecture services considered to be a bad practice. Avoid it if possible.

Invoking service

Service can be invoked using Let<_Service_> method where _Service_ is a type of service that you want to invoke. The Let<> method is available from 2 places:

  • ITecture instance to invoke Tecture service from outer world. See integration section in order to understand how to invoke your Tecture service, e.g. from your ASP.NET MVC application
  • Also Let<> method exists withing each service. Its purpose is to allow inter-communication between services (to invoke one service from another).

Example: invoking one service from another service:

public class B : TectureService<Blueprint>
{
	private B() { }

	public void DoLogicB() { /*...*/ }
}

public class A : TectureService<Blueprint>
{
	private A() { }

	public void DoLogicA()
	{
		Let<B>().DoLogicB();
	}
}

Example: calling service from ITecture injected into web application (see integration section for details)

public class OrdersController : ApiController
{
	private readonly ITecture _tecture;

	public OrdersController(ITecture tecture)
	{
		_tecture = tecture;
	}

	public ActionResult PerformActionWithOrder(int id)
	{
		_tecture.Let<Orders>().PerformAction(id);
		_tecture.Save();

		return Ok();
	}
}

Service lifetime

  • Tecture service instance is being created on demand when Let<> from this service as type parameter is invoked for the first time.
  • Tecture service is disposed when entire ITecture instance is disposed. So if you define ITecture within per-request lifetime scope then all tecture services will exist within this scope. Not more, not less.

Keep this information in mind when defining private variables within service.

What is available inside service?

Service contains primary methods to work with channels

From<> method

This method is invoked with TChannel as type parameter. It obtains Read<TChannel> object that reveals reading end of the channel. Extension methods for reading from channel will be automatically provided by corresponding aspects.

To<> method

This method is invoked with TChannel as type parameter. It obtains Write<TChannel> object that reveals writing (commands) end of the channel. Extension methods for reading from channel will be automatically provided by corresponding aspects (with correspondence to used service tooling).

Methods From<> and To<> are lightweight, so can be safely called as much as needed.

Lifecycle methods

  • Init and Dispose methods are being called when service is going to be created and destroyed correspondingly.
  • OnSave/OnSaveAsync methods are being called when saving/async saving is initiated
  • OnFinally/OnFinallyAsync methods are called after clearing the entire commands queue and all Save iterations passed

OnSave* and OnFinally* can be called several times because of the nature of post-save actions.

Post-save actions

Save and Finally are properties that allows you to perform actions AFTER saving actually happened. You may turn your business logic method into async one and than use await Save and await Finally to enqueue actions that might take place after saving happened or all commands from queue were dispatched (Finally). Please keep in mind that if you enqueue more and more commands after save/finally - you are triggering them to happen and happen again. Be careful, it may be tricky.

Clone this wiki locally