Design Pattern for mapping generic class type to implementation
Hey all,
I'm working on an executor service that maps an executor record of a generic type to an executor that can handle performing operations on that type. Currently, I'm receiving errors on a type conversion in my executor methods for being unable to convert a type and I am not sure how to fix it in my design. I believe this design pattern is similar to mapping a command to a handler, but I am just having trouble implementing it. If you can give me feedback on how to my design or how to do it properly it would help a ton, I have been stuck on this for a bit trying different things.
To start off, I have a ExecutorService thats job is to take an executor record and map it to an executor and return the results. Each executor has it's own implementation with a catch all "object" executor as default. This requires a few components:
1. ExecutorService, which is my service that my controller uses.
2. ExecutorManager, which is my executor manager that registers all executors from DI and allows the executor service to get a executor by key and pass the executor record to that executor to perform an operation.
3. Executor, which is a handler for performing basic crud operations on executor records of a specific type.
4. ExecutorRecord, which is class for storing generic types of data while containing basic information about that data that is common across executors.
5. ExampleRepository, which is a repository that ExecutorRecords will get from DI for what repository to call for basic CRUD operations.
I use my executor service in my controller by creating a record of a specific type and passing it to my ExecutorService with a executor key to map to an specific executor. This approach works well and gives the consumers a nice generic API to work with.
133 Replies
My Executor Service manages mapping operations on records to executors with the ExecutorManager. This allows me to seperate my logic into smaller layers that are easier to work with.
sure as heck sounds like you're just building an IoC container
I was about to say that it sounds just like the command pattern
that too
Now, I have the executors. This is the layer that is giving me the most trouble.
Each executor perform the same operation on executor records, but can have different implementations. I have my BaseExecutor that allows me to have a core implementation that can be overriden. My BaseExecutor implementation gives me errors because I cannot convert the types. If I make my IExecutor CRUD operations non generic I get the convert type errors as well. I'm not sure on how to handle this piece here for connecting it.
oh boy
Oh no
this is starting to look suspiciously like a generic repository
It basically is.
please no
I don't know how else to implement it
I have to have a executor that allows different implementations
why?
Executor records are generic and the data stored in them are different and can go to different locations, which is why they can have different repositories.
different repositories is great
generic repositories that assume all your data access operations will always looks the same is a bad idea
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
^
Basically, each repository connects to its own data container in cosmosdb
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
then, each executor can execute against a certain repository and perform different implementations for crud operations while having a default.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
The regular Sql
CosmosDB
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
The default sql choice when creating the resource
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
Create a container in Azure Cosmos DB for NoSQL using .NET
Learn how to create a container in your Azure Cosmos DB for NoSQL database using the .NET SDK.
I did not put an ORM in front of msft.Data.Sql
My executors contain buisness logic in them too, which is why they can have their own implementations
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
if your executors are business-layer entities, you should not be constraining them to a rudimentary CRUD model
you should be implementing the operations that you actually NEED, on a case-by-case basis
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
Yeah, pretty much. I am not writing any raw SQL
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
ItemResponse<T> response = await container.UpsertItemAsync<Product>(
item: item,
partitionKey: new PartitionKey(item.PartitionKey)
);
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
I have to write out my repository implementation again, but I can. I'm more worried about the executor layer and not the repository.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
I'm not sure. I have to double check. This layer is not the issue though.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
Yeah, I understand that.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
So, if I have 20 different types of records I just write all those extensions?
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
no, you write one extension
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
I feel that I am going the complete opposite direction now.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
O
I'm removing my service completly.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
And writing generic extensions
that is effectively what we're recommending
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
This feels really ugly if I am just writing extensions on top of these which can have different business logic for simple things like calculating patches to stored items.
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
that ALSO gives you the benefit of not forcing yourself to implement methods for types that don't actually need them
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
this doesn't supercede your business layer, it lets you structure it, reasonably, without needing to hamstring yourself with a repository layer
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
I'm trying to figure out how to get this to work without my executors. I just write an extension on my IExecutorRecord<T>?
How does it register my repository?
let's work a specific example
pick one of your record types and one of the operations you need to perform for it
something with a little but of business logic thrown in, like you describe
Here is one:
Then, I want to create a random type:
Unknown User•12mo ago
Message Not Public
Sign In & Join Server To View
heh, roger
although, to be fair, it sounds like you have experience with CosmosDB, and I don't
The implementation does not matter.
so, ultimately, the data record we're storing here is...
My executor just needs anything that meets the requirements of my IRepository<T> interface. It could be a totally different database than cosmosdb.
Yeah, for something super basic.
and looking at the creation....
you're just creating a record with a fresh GUID, saving it, and then returning it?
what does
Name
initialize as?Let's say the value for Name passed was "test".
what's the call/syntax for creating a record in your CosmosDB driver?
I can illustrate what it would be for EF Core....
CreateAsync or something from the library. Sudo code works fine
Yeah, this is way off from what I am able to do.
why so?
I see the thought, but I cannot do it this way sadly.
My implementation is for cosmosdb
Also, it needs to be a service
so, move it to a service
How would you move that to a service?
I would still need to map based off a key for where the data goes and what to do
Or specifying the data type is fine that works too, but I believe that would just be extension methods.
like I said, I'm not familiar with CosmosDB specifics
what is different about interacting with the data store?
I don’t understand how to types to their implementation.
you don't have types an implementations
your type IS the implementation
it's data
For example, if I get data that is general I want to have a default base I use. Then, for know data that I want to do something different I want to map it to a different implementation
Yeah, but if I have 20 different data types I would need 20 different implementations even for things like delete and get that can be generic
yes, that's what you want
you want to be explicit
deduplicate code as you see fit, with extensions
Can I have shared functionality with an abstract or base implementation?
but don't hamstring yourself into assuming that all your different data types must be the same
you're free to do it anyway, but the general wisdom around here is that it doesn't end well
Yeah, that is why I want to be able to override implementations for each individual data type if needed.
Reading your service code it looks like it is the repository code, which confuses me. I don’t understand how it would look for 3 different date types from the caller or service perspective.
do you mean "data" types?
Yeah, “data types”. Sorry on mobile right now and autocorrect kills me haha.
not a problem
looking at the example code in the article you linked, I think the other piece of wisdom for EF applies here as well
you're trying to build yourself a generic repository system on top of something that is ALREADY a generic repository system
How would you make your data service generic above and map to specific implementations?
I still really have no idea what you mean by "specific implementations"
give me an example
Yea, I’m seeing that now, but confused how I can implement it to be a generic service.
you don't
that's the anti-pattern
if you have 20 different data types to provide an API for, you write 20 different services
each one gets to be implemented for only what it needs
you don't end up with coupling between different business classes that shouldn't be coupled
Let’s say we have RecordA, and RecordB. RecordA has a field called Foo that needs to be greater than 3 to be created. Then, let’s say RecordB doesn’t have any constraints to be created. Two different implementations.
two different data models
two different sets of business logic for dealing with them
two different services
or are you saying that both of these come into your application through the same API call?
Yeah, exactly
okay
so, you have an API that looks like....
how do you differentiate
requestData
here?
this one endpoint is supposed to handle creation of all different types of data records?Deserialize data to the correct type based on the caller
what do you mean by "based on the caller"?
The caller tells me the type of record they want to use. So, if they specify RecordA and RecordA is a valid type we can deserialize it to RecordA and pass it to the service to perform operations on it.
okay, so it's part of the request
like, one of the JSON fields
Yeah, the request will contain an annotation on what to do.
so
Yup, exactly.
have you considered that you have re-invented URLs?
Oh my gosh
I see the correlation
so
instead of a request being....
in effect
you let the framework do this type mapping for you
so you write
the "chosing" of which service implemention to use for which data type is solved by you writing out the scenarios for each one, in code, and letting MVC and the IoC container do the mapping, statically
But when I have 20 different data types I will have 20 different controllers. This is where the service comes in handy
to an extent
what you might like is the CQRS pattern
I saw the command bus pattern
your "service" layer becomes something like....
Do you have any sample implementations you can recommend?
and the top layer becomes something like...
and again, the mapping is mainly done with an IoC container
I see. How do I map to multiple different IRequest?
MediatR is the big man in town, for CQRS in .NET
GitHub
GitHub - jbogard/MediatR: Simple, unambitious mediator implementati...
Simple, unambitious mediator implementation in .NET - GitHub - jbogard/MediatR: Simple, unambitious mediator implementation in .NET
Thanks. So, I write the executors and hook them up via DI with mediatR?
essentially
the biggest difference, really, is that the CQRS pattern is more granular
you don't define executors that include a bunch of CRUD repository methods
you define commands/requests
I.E. instead of one executor with 5 operations, 5 request separate handler classes
I see what you are saying
this eliminates the anti-pattern of forcing you to implement basic CRUD operations for all data types
when you may not need them
or they may look wildly different than usual, sometimes
it also still lets you have a base class for "basic" operations
or rather, multiple different base operations
Should I just read through mediatr documentation or is there more I should read for how to implement it this way?
you can define a
BasicCreateRequestHandler
that does it the basic way, and reuse that for, say, 15 of your 20 different data typesI understand what you are saying and where you are going. I feel that you have given me the tools I need and I just need to learn about this new approach and apply
and implement unique ones for the rest
I like that
the MediatR docs are definitely a good place to start
This is exactly what I need
on top of that, just google up CQRS and read whatever looks approachable to you
I don't have any specific recommendations
If you have more for CQRS to I’ll check it out. If not I’ll google around
I appreciate your time and feedback
Thank you a lot
o7
Register your Read/Write methods themselves in DI and inject them in a typesafe way, then your repository can actually be hard typed injected with generic constraints
Do you have an example? I’m kind of confused
So you start off by needing a base class all of your DB entities inherit from to ensure consistency and polymorphism, lets call it BaseEntity and for now all it needs to have is an Id prop that is a string. Now we know all our FooEntities inherit from BaseEntity and everyone has Id.
Next I would define Read and Write models for my given service, so we may have FooEntity (the db model), and then we have:
- FooIndexRequest - Filterable fields for index'ing Foos, implements IIndexRequestBase<FooEntity>
- FooReadResponse - Inherits from FooWriteRequest and tacks on the "readonly" fields
- FooWriteRequest - Used for both PUT and POST, does not have the Id but all writeable fields, implements IWriteRequest<FooEntity>
These typically cover all my bases. Next I add the three following methods:
IIndexRequestBase<T>
IWriteRequest<T>
And then on FooReadResponse
I have this static method:
You can register that third method for DI, and inject that method itself into your Generic Repository, and the other two generic methods are on your models you pass into your methods themselves
This keeps everything type safe and explicit, no casting involved, no weird reflection stuff, everything is exactly what it is and does what it doesI see what you mean. I’m going to give this a try at implementation for fixing my issue. Thanks for the feedback @PixxelKick.