1

In my web app I have a long running operation that is processing some entity in the background. The state of this process should be visible to the clients. During the processing the UI should show something like "entity is being processed" and when it's finished "entity has been processed". My first idea was to model the entity with a state enum. Something like:

public class MyEntity {
    public ProcessingState State { get; set; }
    ... other stuff ...
}

The service method that triggers the processing would have to do something like:

public void Process(int entityId) {
    var entity = _repo.GetEntity(entityId);
    entity.State = State.Processing;
    // here we need to store the entity so that its state is known externally

    ... do the heavy lifting that takes some time ....
    entity.State = State.Processed;
}

This typically is called by a controller that also takes care of the unit-of-work:

 public void Process(int entityId) {
     _service.Process(entityId);
     _unitOfWork.Commit();
 }

However, the storing of the entity just to make its state known to other callers seems to me problematic and it seems to break the unit-of-work pattern.

How do you tackle this?

Ideas:

  • Model as two distinct unit-of-works: 1: update state; commit; 2: do the actual work; commit. But where to put this logic? If it's on the controller level, it needs to be repeated. Somehow it feels as part of the responsibility of the service to update the state.
  • The service is allowed to "bypass" the unit-of-work by directly storing the change on the DB. This probably would work but it feels hackish.

P.S. The controller is simplified here. The processing happens in a background task but conceptually it doesn't make a difference.

3 Answers 3

1

You're missing the singular nature of the unit of work. Effectively, what you want is a context that is used in more than one action:

  • The long running process
  • Any additional request to view the object status while the long running process is still running

Based on your code, you are expecting for the second web request to effectively look into the existing unit of work and read the state. That's not how a unit of work is supposed to work. A unit of work should be scoped to the current execution (running the processing job) and should not be accessed by additional executions (e.g. fetching status information at some unforeseen time)

  1. Model as two distinct unit-of-works: 1: update state; commit; 2: do the actual work; commit.

That is exactly what I would do. This way, the additional requests can open their own unit of work (and context) to the database and retrieve the already updated status. You ensure that everyone who wants to view the data will always have a correct (yet independent) outlook.

But where to put this logic? If it's on the controller level, it needs to be repeated. Somehow it feels as part of the responsibility of the service to update the state.

The controller's responsibility is handling web requests. It does not have the responsibility of executing business logic (other than triggering it in another class/layer of course).

This needs to be added on the business logic level (what I assume you mean with "the service"). Your code example doesn't quite show it, but I assume _repo and _unitOfWork presumably share a common link to the same database context object?

This typically is called by a controller that also takes care of the unit-of-work

That's where I disagree. The controller should not be handling the unit of work. Very simply put, there is an order to the layers:

WEB => BUSINESS => DATA

A unit of work is a wrapper around a db context, or at the very least fills the same niche as a db context would, which makes it part of the DATA layer. The controller should not handle objects that are more than one level down from it. As the controller resides in WEB, it can only directly interact with BUSINESS. It's the responsibility of BUSINESS to handle DATA, and thus the unit of work.

Here's an example of how it would look in the way I create my unit of work:

//WEB (controller)
public void Process(int entityId) 
{
     _service.Process(entityId);
}

//BUSINESS
public void Process(int entityId) 
{
    using(var uow = createUow())
    {
        var entity = uow.EntityRepository.GetEntity(entityId);

        entity.State = State.Processing;
        uow.Commit();

        // ... do the heavy lifting that takes some time ....

        entity.State = State.Processed;
        uow.Commit();
    }
}

//DATA
public class UnitOfWork : IDisposable
{
    private readonly MyContext _context;      

    public EntityRepository EntityRepository { get; private set; }

    public UnitOfWork()
    {
        _context = new MyContext();
        EntityRepository = new EntityRepository(_context);
    }

    public void Commit()
    {
       _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

You didn't post your unit of work implementation so I simply used my approach here to complete the example. The focus of the answer isn't on how the uow is built, but rather how it is used by the business layer (and not the web layer).

  1. The service is allowed to "bypass" the unit-of-work by directly storing the change on the DB. This probably would work but it feels hackish.

I'm not quite sure what benefit this would bring to the table. Why would this be better than simply doing an early commit of your unit of work?

5
  • We have been shifting the UoW pattern to the controllers because one service itself cannot decide on its own if the whole UoW has been accomplished or not. There might be multiple service calls that compose the UoW. Unless you start introducing higher-level services which call out to other lower-level services for every UoW. Is that how you do it?
    – Dejan
    Mar 4, 2019 at 14:19
  • 1
    @Dejan: The composition of multiple "subtasks" (= multiple service calls) also belongs to the Business layer, as far as I'm concerned. The main goal of having this in the business layer is to ensure that if there are multiple applications which need to be able to do the same big task (= set of subtasks), that they all rely on the same reusable Business layer, as opposed to each application having to make sure they compose the subtasks the exact same way.
    – Flater
    Mar 4, 2019 at 15:28
  • @Dejan: In short, your comment implies that a layer can only have one level of abstraction, but that is not correct. It can have multiple levels (e.g. services for subtasks, and services for composed tasks which depend on those subtask services).
    – Flater
    Mar 4, 2019 at 15:29
  • I didn't want to imply that a layer can only have one level of abstractation. I was just wondering then if you would kind of replicate every controller method into one high-level service layer method.
    – Dejan
    Mar 4, 2019 at 19:48
  • @Dejan: You're describing it too vaguely for me to accurately answer. No, the whole method shouldn't be moved, since part of the method's body is obviously the controller's responsibility. What needs to happen is that you separate the actual controller's responsibility from the business logic responsibility, and move the latter up to the business layer. Is it possible that controller methods will then map uniquely to a business method that only they use? Yes. Is it a requirement for it to be this way? No.
    – Flater
    Mar 5, 2019 at 6:48
1

Work with application events and/or a message queue. Your current approach inevitably leads to polling. You achieve much greater scalability and user experience with an event-based approach.

4
  • Ultimately, we do have SignalR involved too which raises events when the entity.State changes. But then again, one needs to also persist the current state. Otherwise how can a newly joining client know what the current state is when events/messages passed already?
    – Dejan
    Mar 4, 2019 at 14:23
  • @Dejan you could store a flag/timestamp when completed. When a new client joins they can check for the flag. if its missing, they subscribe to the completion message. If you want to avoid race-conditions you can check for the flag/timestamp again after subscribing. This way you can still commit your unit of work with the flag set only once when the processing is done.
    – marstato
    Mar 4, 2019 at 15:46
  • then I would also need another timestamp to distinguish between ReadyToBeProcessed and Processing. So eventually, we're coming back to having a State field which persists the current state of the entity.
    – Dejan
    Mar 4, 2019 at 19:46
  • @Dejan what makes an entity ready for processing? If that involves anything other than the consent of a user, it is actually part of your processing. If it only requires the consent of a user, you don't need to notify that user that the entity is now ready - they just gave their consent. In either case marking the entity as ready happens in another unit of work as the start of the processing. Otherwise, again, it's part of the processing.
    – marstato
    Mar 4, 2019 at 20:49
-1

I've ended up using the following pattern. In the controller, I would:

public void Process(int entityId, BackgroundTaskQueue queue) {
    var entity = GetEntityById(entityId);
    entity.State = State.Processing;
    queue.Queue((uow) => {
        _service.Process(entity);
        uow.Commit();
    );
    _unitOfWork.Commit();
}

And in the service:

public void Process(Entity entity) {
   ... do the heavy lifting that takes some time ....
   entity.State = State.Processed;
}

Therefore, there are two units of work. The first sets the state to Processing. And the second is executed in the background and when finished, sets the state to Processed.

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.