I have given the following architecture:
Layers:
Bootstrapper
MyApp.Start
- IoC-Container etc.
UI Layer
MyApp.Gui
- Views
- ViewModels
Business Layer
MyApp.Core
- DataGenerator
- DataGeneratorRequest
- DataGeneratorResponse
MyApp.Core.Interfaces
- IDataGenerator
- IDataGeneratorRequest
- IDataGeneratorResponse
There is a method Generate
inside IDataGenerator
which takes IDataGeneratorRequest
as a parameter and returns IDataGeneratorResponse.
Problem:
At the moment the IoC Container layer can feed my UI with an IDataGenerator, but I would still need to reference the Core-Project to build a request and throw it into the Generate
-method whenever it's necessary. I don't like that and would like to get rid of the dependency.
In a perfect world, how would it be architectured?
- Was it a good thing to create interfaces for my requests/responses in the first place?
- Do the request- and response-classes belong into the Core.Interface-project instead?
- Or should I add a third project, which has all classes in it, that are exchanged?
1 Answer 1
Was it a good thing to create interfaces for my requests/Responses in the first place?
That's hard to say. It really depends on how complex your request/response objects are, or if they require some internal hooks to the implementation of your IDataGenerator
. It provides the most flexibility.
Do the request- and response-classes belong into the Core.Interface-project instead?
Depends on the answer to the first question. If they have internal hooks to your DataGenerator
then no. If they are completely autonomous simple objects, then that would be the simplest option.
Or should I add a third project, which has all classes in it, that are exchanged?
Probably not.
You have some options, and what you choose really depends on how complex your request/response objects are. The first thing that you have to decide for your MyApp.Core.Interfaces
project is whether you are going to be strict about everything being a C# interface
, or if you can allow simple objects for your request/response. The bottom line is that consumers of your IDataGenerator
need to be able to create a request object instance.
Option 1: Keep it all literal interfaces
To do this, you need to add a mechanism to create your request. The simplest way to do that is to either pass in a lambda or provide a IDataGenerator.CreateRequest()
method. The new Net Core APIs tend to use the lambda approach:
IDataGeneratorResponse Generate(Action<IDataGeneratorRequest> getRequest);
That would be called something like this:
var response = dataGenerator.Generate(request => {
request.DataType = DataType.MockReport;
request.From = DateTime.Now.AddDays(-7);
request.To = DateTime.Now;
});
The implementation side would look something like this:
public IDataGeneratorResponse Generate(Action<IDataGeneratorRequest> getRequest)
{
// NOTE: assumption is that DataGeneratorRequest is a class and
// not a struct
var request = new DataGeneratorRequest();
getRequest(request);
// ... do all the data generation
return new DataGeneratorResponse(calculatedData);
}
Of course, you can be free to make it more of a fluent API if you so choose. Either approach will work.
Option 2: Keep it logically interfaces
In this case you would either have a fully developed DataGeneratorRequest
class, or you would have a factory to create them. I understand that you may not be able to have the full implementation of the DataGeneratorRequest
class in your MyApp.Core.Interfaces
project, which is why you would need a factory in that case.
The hybrid approach would be to make the base class for DataGeneratorRequest
have a protected constructor and a public factory method like this:
public abstract class DataGeneratorRequest : IDataGeneratorRequest
{
protected DataGeneratorRequest() {};
// .... the rest of the implementation
public static IDataGneratorRequest Create()
{
// You'll have to use reflection here to create the instance
// and a way for your full implementation to inform the factory
// what to instantiate.
}
}
Option 3: Reference both Interfaces and Implementation
The last option is to rely on the fact that you will have your implementation of the IDataGenerator referenced in your final project as well as the interfaces. You have your DataGeneratorRequest
implementation fully developed in the implementation assembly and simply create that like you would any other object:
var request = new DataGeneratorRequest
{
DataType = DataType.MockReport,
From = DateTime.Now.AddDays(-7),
To = DateTime.Now
};
var response = dataGenerator.Generate(request);
This is the simplest to implement, but you won't be able to build things to consume your data generator without referencing the implementation assembly. That might not be how you want to organize your application, particularly if you want to separate the consumers of your interface and load them dynamically.
Tradeoffs
- Option 1 will ensure that consumers will only need the
MyApp.Core.Interfaces
assembly to do it's work. It will also make sure that they are designed to consume the same version of theIDataGeneratorRequest
object. There's a manageable amount of complexity, and no reflection. - Option 2 will either force your
DataGeneratorRequest
implementation to be very simple, or you will need reflection. In either case, the API is still very usable and will only require a reference to theMyApp.Core.Inferfaces
assembly. - Option 3 is the easiest to implement, but adds complexity to the consumers of the API. This is primarily because the API is not fully specified in the
MyApp.Core.Interfaces
assembly, and requires another assembly to be referenced. You loose flexibility, but you might have subtle errors if you have different assemblies for consumers that were built against slightly different versions of the implementation assembly.
To be honest, I am more partial to the first option. It feels more natural to the way Net Core apps are set up and configured these days.
-
Thank you for this brief explaination, it really gave me some ideas. I am really curious about option 1. How would the other side look like, the "Generate" methods implementation, can you add a small simple example of an implementation? I wouldn't even need a request-class anymore, would I? When you talk about "internal hooks", what exactly do you mean by that?Jannik– Jannik2018年07月02日 14:20:24 +00:00Commented Jul 2, 2018 at 14:20
-
@Jannik, internal hooks would be if there was something that was package public that your Request called directly into your DataGenerator. In other words, it relies on code outside of itself that is only accessible to classes in the same assembly. While it's not ideal, sometimes it does have to be done.Berin Loritsch– Berin Loritsch2018年07月02日 14:25:08 +00:00Commented Jul 2, 2018 at 14:25
-
Unfortunately, I think, I still haven't understood, what an internal hook is :( But I can say that my requests will only be some simple DTOs. :)Jannik– Jannik2018年07月02日 14:39:06 +00:00Commented Jul 2, 2018 at 14:39
-
1If you make any
internal
calls, then those are internal hooks: docs.microsoft.com/en-us/dotnet/csharp/language-reference/… . Good news is that if your objects are simple DTOs, you could include them in theMyApp.Core.Interfaces
assembly and the request/response interfaces would then be superfluous.Berin Loritsch– Berin Loritsch2018年07月02日 14:58:18 +00:00Commented Jul 2, 2018 at 14:58 -
I have all the Information I needed, I will consider them and will decide whether to move the classes to Core.Interfaces or just try it out for practice to implement your first option. Thank you.Jannik– Jannik2018年07月02日 15:07:02 +00:00Commented Jul 2, 2018 at 15:07