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?
-
1Whenever 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– Flater06/07/2022 20:28:07Commented Jun 7, 2022 at 20:28
-
@Flater, yes, it makes sense. I can definitely do that.the-4th– the-4th06/07/2022 21:40:33Commented 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– John Wu07/12/2022 01:34:11Commented Jul 12, 2022 at 1:34
-
No, it's not WCF. It's a standard REST API with .Net Core.the-4th– the-4th07/13/2022 03:46:38Commented Jul 13, 2022 at 3:46
1 Answer 1
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.
-
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– the-4th06/08/2022 02:22:47Commented 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 wayEwan– Ewan06/08/2022 11:17:22Commented Jun 8, 2022 at 11:17
Explore related questions
See similar questions with these tags.