-
Notifications
You must be signed in to change notification settings - Fork 13
Services
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.
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.
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();
}
}
- 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 defineITecture
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.
Service contains primary methods to work with channels
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.
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.
-
Init
andDispose
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 allSave
iterations passed
OnSave*
and OnFinally*
can be called several times because of the nature of 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.
(c) 2020, Pavel B. Novikov