0

I am designing an API using .NET core 6 C# which has 2 versions. For v1 I have something like the following for the business logic interface (fictional names and simple fields just to facilitate understanding):

public interface IV1Business
{
    Task<MyModel> Create(string configuration, string metadata);

    Task<MyModel> GetOne(string id);

    Task<IEnumerable<MyModel>> GetAll(IEnumerable<string> ids);

    Task Update(string id, string, configuration, string metadata);

    Task Delete(string id);
}

Then I have 3 implementations A, B and C.

But not all of them follow the same contract, for example, C is allowed to operate with Get, B can do Get and Update, and A has everything, which produces the following:

public class C : IV1Business
{        
    public async Task<MyModel> Create(string configuration, string metadata)
    {
        throw new NotImplementedException();
    }

    public async Task<MyModel> GetOne(string id)
    {
      // implementation
    }

    public async Task<IEnumerable<MyModel>> GetAll(IEnumerable<string> ids)
    {
      // implementation
    }

    public async Task Update(string id, string, configuration, string metadata)
    {
       throw new NotImplementedException();
    }

    public async Task Delete(string id)
    {
       throw new NotImplementedException();
    }
}

This is not good, it does not make sense to have a contract where there are holes like that.

So, I have applied the interface segregation principle (ISP) and extracted some interfaces so starting by minimum operations (C) until the use case of A which does everything.

public interface IV1GetAllBusiness
{
    Task<MyModel> GetOne(string id);

    Task<IEnumerable<MyModel>> GetAll(IEnumerable<string> ids);
}

public interface IV1UpdateBusiness
{
    Task Update(string id, string, configuration, string metadata);
}

public interface IV1GetAndUpdateBusiness : IV1GetAllBusiness, IV1UpdateBusiness
{        
}

public interface IV1Business : IV1GetAndUpdateBusiness 
{
    Task<MyModel> Create(string configuration, string metadata);

    Task Delete(string id);
}

For v2, the contract has changed, and now there are different routes to update the configuration and metadata of MyModel, which makes me believe v1 and v2 are incompatible and they need different interfaces.

So my question is, does it make sense to have this approach for v1 using some sort of role bases interfaces and avoid the throws for not-implemented methods?

Is there any other approach that would fit well on this situation for multiple versions of the API?

4
  • 1
    Whenever you find yourself subdividing your names using a prefix, as is the case with using v1 here, consider using namespaces and project folders instead. In this case, a v1 subfolder and subnamespace makes a lot of sense.
    – Flater
    Commented Jun 7, 2022 at 20:28
  • @Flater, yes, it makes sense. I can definitely do that.
    – the-4th
    Commented Jun 7, 2022 at 21:40
  • What is the transport for the API calls? Is it WCF? Or is this an in-process DLL sort of API?
    – John Wu
    Commented Jul 12, 2022 at 1:34
  • No, it's not WCF. It's a standard REST API with .Net Core.
    – the-4th
    Commented Jul 13, 2022 at 3:46

1 Answer 1

2

I get that you cant give real examples, but its basically impossible to give more than general advice without them.

Here I would say that if you are down to one method per interface, something has gone wrong. How is the calling code supposed to use these objects if it doesnt even know what it can do with them?

Possibly try and invert the problem with a "tell don't ask" approach. Instead of having some code call multiple methods on the logic class, raise an even with the data and have the logic classes process that. Then they can decide if they need to call get or delete or whatever and obviously if they don't support those functions they wont need them.

So...

MyVersionWhataverImplementationWhateverLogic : IBusinessProcessForBuyEvents
{
    public void ProcessBusinessEvent(BuyEvent e)
    {
        //load X
        //delete Y
        //kick off process C

    }
}

Now I can go make many of these with the same interface and it can handle all sorts of versions of the process. I can load extra data through constructors which are not part of interfaces.

If the event changes, which hopefully is less often than thinking of new ways to process it, well i can either expand the event class and recompile, or inherit from v1 to make a v2 event, or just make a new event and interface.

2
  • by "if they don't support those functions they wont need them.", do you mean the actual implementations not necessarily need the interfaces I first shared on my post?
    – the-4th
    Commented Jun 8, 2022 at 2:22
  • i mean if the implementation didnt implement delete in your model, it wont need to delete when it handles the event in mine. because i "inverted the control" delete isnt exposed either way
    – Ewan
    Commented Jun 8, 2022 at 11:17

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