1

My model classes (business layer/library of ASP .NET core solution) uses a number of services (IOrmService, IEmailService, IFileService, IHtmlToPdfConverter etc.). Different models requires different combinations of the services for CRUD operations. It prevents me to use base class to max extent and forces to repeat code/methods that are very similar. It could be solved by passing not a combination of services to factory methods but a container with all of the services. Which solution should I chose: - inject IServiceProvider (looks like a bit too tight coupling with ASP); - create and inject my own service container (e.g. IBusinessServiceProvider); - leave everything as is.

  • My first critique is that your interfaces are two technology specific. They should be domain specific, or use the terminology of your users. For example, IHtmlToPdfConverter can be IDocumentConverter etc. Second, the implementations of those services can be made up of their own things, but the core business should not have to know about them. Those implementations would either be in their own libraries, or implemented at the edge (in the ASP.Net layer). – Berin Loritsch Jan 6 at 14:21
  • 1
    The Repository Pattern would replace your IOrmService. In other words, you would need a repository for each business object you intend to work with. Your business logic would then go to the repository for that business object. You can use generics to enforce common method patterns, etc. across the board. – Berin Loritsch Jan 6 at 14:23
  • Good point regarding the naming. I'll change that. Just one addition just in case - in my specific case those services are logically part of business procedures, e.g. saving an invoice and sending it to a client should only happen within the same transaction. If send fails, the transaction shall be rollbacked. – Hobbyist Jan 6 at 15:05
  • "The Repository Pattern would replace your IOrmService." - could replace, but I prefer using factory methods on model instead. Yet the problem stays the same, only changes location from model classes to repository classes. – Hobbyist Jan 6 at 15:09
1

Move the code that uses these services outside of your models.

If you keep your models pure logic and put the interfaces at the top level of the application they will be easier to read, test and edit.

eg instead of

public class Model
{
    private IFileService fs;
    public SpellCheck(string filename)
}

Have:

public class Model
{
    public SpellCheck(IStream file)
}

public class App
{
    private IFileService fs;
    public void Main()
    {
        var stream = fs.load(filename);
        Model.SpellCheck(stream);
    }
}

Now the classes are even more decoupled than they were when injecting services and you don't have the problem of injecting services just because some sub object has one method which uses them.

  • I do use factory methods and inject the dependencies in methods like you described (the repository pattern is too "heavy" for my simple solution). However, it still prevents me to generalize common behavior in base class. E.g. I have methods: Invoice.Save(IOrmService ormService, IEmailService mailer, IFileService fileService) Court.Save(IOrmService ormService) etc. Yet I could avoid code duplication if I provided a common method in base class: Save(IBusinessServiceProvider service) – Hobbyist Jan 6 at 14:57
  • I'm saying get rid of the Save methods – Ewan Jan 6 at 14:58
  • How? If i move it to some other class (repository), i wouldn't solve the problem. Different save methods shall be required for different models even though their internal are very similar. – Hobbyist Jan 6 at 15:14
  • it solves your problem of having to inject stuff. whats unclear is what other problem you think it will cause – Ewan Jan 6 at 15:29
  • I could generalize the save method for all of the model classes in base class like that: pastebin.com/xihAmccB The services except for the IOrmSevice and ICacheProvider are only used by concrete model implementations. Those could override virtual methods AuthorizeSave, BeforeSaveAsync, SaveChildrenAsync and AfterSaveAsync to implement custom persistence logic (if required). Yet different models require different services which brings us to different Save method signature. – Hobbyist Jan 6 at 15:47
1

As an outsider looking in, it appears that you are struggling with the design of the components. Breaking down actionable functions that users need to perform into a sensible set of things can be done a myriad of ways. It seems like the current breakdown is somewhere between domain driven design and functional design. Without more knowledge of your domain, it's hard to provide specific examples. So, in my answer I'd like to discuss how I approach the problem.

Let's say we have this requirement:

Given we have a report

When an analyst publishes it

Then the report must be sent as a PDF to the manager, and the group that asked for the report

I start to think about what are the nouns? I've basically have been given the noun of Report and a number of Users that have different roles. Then I think about what are my verbs? In this scenario, I have the verb publish but the meanings are implied. I also have the verb send, but this seems like that verb is something the publish verb does as a step.

Next I start thinking what else do I need to make this happen? So implied in this scenario is something that manages reports. There are common patterns for what "managing" something means, so I opt for the repository pattern and have a ReportRepository. Also, I need to know how to find users and verify them as well. So that implies I need a UserRepository and potentially a Role field in the User object. To get the "group that asked for the report" then either the Report needs to keep track of the Requester or I need to query the ReportRegistry how to get the requester.

OK so let's look at what we have now:

public interface IReportRepository {
    bool publish(Report report, User fromUser);
}

public interface IUserRepository {
    User findManager(User fromUser);
}

public class Report {
    User getRequester();
}

public class User {
    Role getRole();
}

OK, I can start to see things take shape. I personally prefer to use Dependency Injection as opposed to the Factory pattern, because I may have to implement something outside the core business logic. One such example would be the thing that sends the report to the specific users. Also, since the format of the report is specified in my requirements, it's likely I may have to convert form one format to another. In that case, we need to look at detecting the report format and doing a conversion.

At this point I need to pause and think about where the report gets converted. Does the notifier take a Report object and convert if necessary? or does the ReportRepository take care of that for you? Both are valid ways of doing this. For the sake of argument, I am going to make the executive decision that the thing that distributes the report worries about that. That means I only need to add one more noun into my central business objects:

public enum Format {
    HTML,
    PDF,
    Word
}

public interface IReportDistributor {
    public bool send(Report report, Format format,
                     User fromUser, param User... toUsers);
}

// and since we would need to know the format of the report, Report gets new methods.
public class Report {
    User getRequester();
    Format getFormat();
    OutputStream getContent();
}

Now, when I implement my IReportRepository I know it needs an IReportDistributor and a IUserRepository. Since this is core functionality in my business logic, the implementation class also goes here:

public class RepositoryManager : implements IRepositoryManager {
    private final IUserRepository userRepository;
    private final IReportDistributor distributor;

    public RepositoryManager(IUserRepository users, IReportDistributor distributor) {
        this.userRepository = users;
        this.distributor = distributor;
    }

    public bool publish(Report report, User fromUser) {
        if (fromUser.Role != Role.Analyst) {
            return false;
        }

        User toManager = userRepository.findManager(fromUser);
        User toRequestor = report.getRequestor();

        return distributor.send(report, Format.PDF, fromUser, toManager, toRequestor);
    }
}

That's the whole implementation from the business perspective. Now, the implementation of the IReportDistributor is made closer to the edge, because we are choosing to implement it outside of the whole system.

You have enough here in the business logic to test the business rule in the application. You just need to supply a mock or stub instance of the IReportDistributor to validate that the parameters are filled out exactly like you expect.

For the sake of brevity, I will leave the conversion code implementation and email implementation of report distribution to you. That would be in it's own DLL since those things are complex enough.

  • Ok, you create a repository per model. Than pretty much all of the repositories will have method Save. As the save operation requires different services per model, each of the repositories shall have different constructor signatures. Yet the Save method will contain very similar code for each model which is what I want to avoid. Or shouldn't I? – Hobbyist Jan 6 at 16:24
  • You can use generics to address a lot of those commonalities. – Berin Loritsch Jan 6 at 17:10
  • All the repositories should be added in ConfigureServices? – Hobbyist Jan 7 at 10:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.