C
C#•7mo ago
Bluestopher

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.
c#
[ApiController]
[Route("[controller]")]
public class ExecutorController : ControllerBase
{
private readonly ILogger<ExecutorController> _logger;

private readonly IExecutorService _executorService;

public ExecutorController(ILogger<ExecutorController> logger, IExecutorService executorService)
{
Requires.NotNull(logger, nameof(logger));
Requires.NotNull(executorService, nameof(executorService));

_logger = logger;
_executorService = executorService;
}

[HttpPost(Name = "CreateRecord")]
public IActionResult Create()
{
ExampleExecutorData exampleExecutorData = new ExampleExecutorData();
ExecutorRecord<object> executorRecord = new ExecutorRecord<object>(Guid.NewGuid(), exampleExecutorData);

IExecutorRecord<object> createdRecord = _executorService.Create(executorRecord, ExecutorKey.Default).Result;

return new JsonResult(createdRecord);
}
}
c#
[ApiController]
[Route("[controller]")]
public class ExecutorController : ControllerBase
{
private readonly ILogger<ExecutorController> _logger;

private readonly IExecutorService _executorService;

public ExecutorController(ILogger<ExecutorController> logger, IExecutorService executorService)
{
Requires.NotNull(logger, nameof(logger));
Requires.NotNull(executorService, nameof(executorService));

_logger = logger;
_executorService = executorService;
}

[HttpPost(Name = "CreateRecord")]
public IActionResult Create()
{
ExampleExecutorData exampleExecutorData = new ExampleExecutorData();
ExecutorRecord<object> executorRecord = new ExecutorRecord<object>(Guid.NewGuid(), exampleExecutorData);

IExecutorRecord<object> createdRecord = _executorService.Create(executorRecord, ExecutorKey.Default).Result;

return new JsonResult(createdRecord);
}
}
133 Replies
Bluestopher
Bluestopher•7mo ago
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.
c#
public interface IExecutorService
{
Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Get<T>(Guid id, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Delete<T>(Guid id, ExecutorKey key = ExecutorKey.Default);
}

public class ExecutorService : IExecutorService
{
private readonly ExecutorManager _executorManager;

public ExecutorService(ExecutorManager executorManager) {
Requires.NotNull(executorManager, nameof(executorManager));

_executorManager = executorManager;
}

public Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default)
{
if (Enum.IsDefined(typeof(ExecutorKey), key))
throw new ArgumentException($"Invalid executor key: \"{key}\".");
Requires.NotNull(record, nameof(record));

IExecutor executor = _executorManager.GetExecutor<T>(key);

return executor.Create(record);
}

public Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Get<T>(Guid id, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Delete<T>(Guid id, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();
}

public interface IExecutorManager
{
IExecutor GetExecutor<T>(ExecutorKey key);
}

public class ExecutorManager : IExecutorManager
{
private Dictionary<ExecutorKey, IExecutor> _executors = new();

public ExecutorManager(IEnumerable<IExecutor> executors)
{
Requires.NotNull(executors, nameof(executors));

foreach (IExecutor executor in executors)
{
RegisterExecutor(executor);
}
}

private void RegisterExecutor(IExecutor executor)
{
Requires.NotNull(executor, nameof(executor));

if(_executors.ContainsKey(executor.Key))
{
throw new ArgumentException($"key \"{executor.Key}\" has already registered an executor.");
}

_executors.Add(executor.Key, executor);
}

public IExecutor GetExecutor<T>(ExecutorKey key)
{
if (_executors.ContainsKey(key))
{
throw new ArgumentException($"key \"{key}\" does not have a registered executor.");
}

return _executors[key];
}
}
c#
public interface IExecutorService
{
Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Get<T>(Guid id, ExecutorKey key = ExecutorKey.Default);
Task<IExecutorRecord<T>> Delete<T>(Guid id, ExecutorKey key = ExecutorKey.Default);
}

public class ExecutorService : IExecutorService
{
private readonly ExecutorManager _executorManager;

public ExecutorService(ExecutorManager executorManager) {
Requires.NotNull(executorManager, nameof(executorManager));

_executorManager = executorManager;
}

public Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default)
{
if (Enum.IsDefined(typeof(ExecutorKey), key))
throw new ArgumentException($"Invalid executor key: \"{key}\".");
Requires.NotNull(record, nameof(record));

IExecutor executor = _executorManager.GetExecutor<T>(key);

return executor.Create(record);
}

public Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Get<T>(Guid id, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Delete<T>(Guid id, ExecutorKey key = ExecutorKey.Default) => throw new NotImplementedException();
}

public interface IExecutorManager
{
IExecutor GetExecutor<T>(ExecutorKey key);
}

public class ExecutorManager : IExecutorManager
{
private Dictionary<ExecutorKey, IExecutor> _executors = new();

public ExecutorManager(IEnumerable<IExecutor> executors)
{
Requires.NotNull(executors, nameof(executors));

foreach (IExecutor executor in executors)
{
RegisterExecutor(executor);
}
}

private void RegisterExecutor(IExecutor executor)
{
Requires.NotNull(executor, nameof(executor));

if(_executors.ContainsKey(executor.Key))
{
throw new ArgumentException($"key \"{executor.Key}\" has already registered an executor.");
}

_executors.Add(executor.Key, executor);
}

public IExecutor GetExecutor<T>(ExecutorKey key)
{
if (_executors.ContainsKey(key))
{
throw new ArgumentException($"key \"{key}\" does not have a registered executor.");
}

return _executors[key];
}
}
JakenVeina
JakenVeina•7mo ago
sure as heck sounds like you're just building an IoC container
Pobiega
Pobiega•7mo ago
I was about to say that it sounds just like the command pattern
JakenVeina
JakenVeina•7mo ago
that too
Bluestopher
Bluestopher•7mo ago
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.
c#
public interface IExecutor
{
ExecutorKey Key { get; }

Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Get<T>(Guid id);

Task<IExecutorRecord<T>> Delete<T>(Guid id);
}

public class BaseExecutor<T> : IExecutor
{
public ExecutorKey Key { get; }

private IRepository<T> _repository { get; }

public BaseExecutor(ExecutorKey key, IRepository<T> repository)
{
if (Enum.IsDefined(typeof(ExecutorKey), key))
throw new ArgumentException($"Invalid executor key: \"{key}\".");
Requires.NotNull(repository, nameof(repository));

Key = key;
_repository = repository;
}

public virtual Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record)
{
Requires.NotNull(record, nameof(record));

return _repository.Create(record);
}

public virtual Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Get<T>(Guid id) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Delete<T>(Guid id) => throw new NotImplementedException();
}

public class DefaultExecutor : BaseExecutor<object>, IExecutor
{
public DefaultExecutor(IRepository<object> repository) : base(ExecutorKey.Default, repository) { }
}

public enum ExecutorKey
{
[Description("Default")]
Default = 1,
}
c#
public interface IExecutor
{
ExecutorKey Key { get; }

Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Get<T>(Guid id);

Task<IExecutorRecord<T>> Delete<T>(Guid id);
}

public class BaseExecutor<T> : IExecutor
{
public ExecutorKey Key { get; }

private IRepository<T> _repository { get; }

public BaseExecutor(ExecutorKey key, IRepository<T> repository)
{
if (Enum.IsDefined(typeof(ExecutorKey), key))
throw new ArgumentException($"Invalid executor key: \"{key}\".");
Requires.NotNull(repository, nameof(repository));

Key = key;
_repository = repository;
}

public virtual Task<IExecutorRecord<T>> Create<T>(IExecutorRecord<T> record)
{
Requires.NotNull(record, nameof(record));

return _repository.Create(record);
}

public virtual Task<IExecutorRecord<T>> Update<T>(IExecutorRecord<T> record) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Patch<T>(IExecutorRecord<T> record) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Get<T>(Guid id) => throw new NotImplementedException();

public virtual Task<IExecutorRecord<T>> Delete<T>(Guid id) => throw new NotImplementedException();
}

public class DefaultExecutor : BaseExecutor<object>, IExecutor
{
public DefaultExecutor(IRepository<object> repository) : base(ExecutorKey.Default, repository) { }
}

public enum ExecutorKey
{
[Description("Default")]
Default = 1,
}
JakenVeina
JakenVeina•7mo ago
oh boy
Bluestopher
Bluestopher•7mo ago
Oh no
JakenVeina
JakenVeina•7mo ago
this is starting to look suspiciously like a generic repository
Bluestopher
Bluestopher•7mo ago
It basically is.
JakenVeina
JakenVeina•7mo ago
please no
Bluestopher
Bluestopher•7mo ago
I don't know how else to implement it I have to have a executor that allows different implementations
JakenVeina
JakenVeina•7mo ago
why?
Bluestopher
Bluestopher•7mo ago
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.
c#
public interface IRepository<T>
{
Task<IExecutorRecord<T>> Create(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Update(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Patch(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Get(Guid id);

Task<IExecutorRecord<T>> Delete(Guid id);
}

public class ExampleRepository<T> : IRepository<T>
{
public Task<IExecutorRecord<T>> Create(IExecutorRecord<T> record) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Delete(Guid id) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Get(Guid id) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Patch(IExecutorRecord<T> record) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Update(IExecutorRecord<T> record) => throw new NotImplementedException();
}

public interface IExecutorRecord<T>
{
Guid Id { get; }

T Data { get; }
}

public class ExecutorRecord<T> : IExecutorRecord<T>
{
public Guid Id { get; set; }

public T Data { get; set; }

public ExecutorRecord(Guid id, T data)
{
Id = id;
Data = data;
}
}

public class ExampleExecutorData
{
string name { get; }
}
c#
public interface IRepository<T>
{
Task<IExecutorRecord<T>> Create(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Update(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Patch(IExecutorRecord<T> record);

Task<IExecutorRecord<T>> Get(Guid id);

Task<IExecutorRecord<T>> Delete(Guid id);
}

public class ExampleRepository<T> : IRepository<T>
{
public Task<IExecutorRecord<T>> Create(IExecutorRecord<T> record) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Delete(Guid id) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Get(Guid id) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Patch(IExecutorRecord<T> record) => throw new NotImplementedException();

public Task<IExecutorRecord<T>> Update(IExecutorRecord<T> record) => throw new NotImplementedException();
}

public interface IExecutorRecord<T>
{
Guid Id { get; }

T Data { get; }
}

public class ExecutorRecord<T> : IExecutorRecord<T>
{
public Guid Id { get; set; }

public T Data { get; set; }

public ExecutorRecord(Guid id, T data)
{
Id = id;
Data = data;
}
}

public class ExampleExecutorData
{
string name { get; }
}
JakenVeina
JakenVeina•7mo ago
different repositories is great generic repositories that assume all your data access operations will always looks the same is a bad idea
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
^
Bluestopher
Bluestopher•7mo ago
Basically, each repository connects to its own data container in cosmosdb
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
then, each executor can execute against a certain repository and perform different implementations for crud operations while having a default.
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
The regular Sql
Pobiega
Pobiega•7mo ago
CosmosDB
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
The default sql choice when creating the resource
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
Bluestopher
Bluestopher•7mo ago
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
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
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
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
Yeah, pretty much. I am not writing any raw SQL
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
ItemResponse<T> response = await container.UpsertItemAsync<Product>( item: item, partitionKey: new PartitionKey(item.PartitionKey) );
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
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
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
I'm not sure. I have to double check. This layer is not the issue though.
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
Yeah, I understand that.
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
So, if I have 20 different types of records I just write all those extensions?
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
no, you write one extension
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
I feel that I am going the complete opposite direction now.
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
O I'm removing my service completly.
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
And writing generic extensions
JakenVeina
JakenVeina•7mo ago
that is effectively what we're recommending
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
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
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
that ALSO gives you the benefit of not forcing yourself to implement methods for types that don't actually need them
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
this doesn't supercede your business layer, it lets you structure it, reasonably, without needing to hamstring yourself with a repository layer
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
Bluestopher
Bluestopher•7mo ago
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?
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
Here is one:
c#
public interface IExecutorRecord<T>
{
Guid Id { get; }

T Data { get; }
}

public class ExecutorRecord<T> : IExecutorRecord<T>
{
public Guid Id { get; set; }

public T Data { get; set; }

public ExecutorRecord(Guid id, T data)
{
Id = id;
Data = data;
}
}

public class ExampleExecutorData
{
string name { get; }
}
c#
public interface IExecutorRecord<T>
{
Guid Id { get; }

T Data { get; }
}

public class ExecutorRecord<T> : IExecutorRecord<T>
{
public Guid Id { get; set; }

public T Data { get; set; }

public ExecutorRecord(Guid id, T data)
{
Id = id;
Data = data;
}
}

public class ExampleExecutorData
{
string name { get; }
}
Then, I want to create a random type:
c#
[HttpPost(Name = "CreateRecord")]
public IActionResult Create()
{
ExampleExecutorData exampleExecutorData = new ExampleExecutorData();
ExecutorRecord<ExampleExecutorData > executorRecord = new ExecutorRecord<ExampleExecutorData >(Guid.NewGuid(), exampleExecutorData);

IExecutorRecord<ExampleExecutorData > createdRecord = _executorService.Create(executorRecord, ExecutorKey.Default).Result;

return new JsonResult(createdRecord);
}
c#
[HttpPost(Name = "CreateRecord")]
public IActionResult Create()
{
ExampleExecutorData exampleExecutorData = new ExampleExecutorData();
ExecutorRecord<ExampleExecutorData > executorRecord = new ExecutorRecord<ExampleExecutorData >(Guid.NewGuid(), exampleExecutorData);

IExecutorRecord<ExampleExecutorData > createdRecord = _executorService.Create(executorRecord, ExecutorKey.Default).Result;

return new JsonResult(createdRecord);
}
Unknown User
Unknown User•7mo ago
Message Not Public
Sign In & Join Server To View
JakenVeina
JakenVeina•7mo ago
heh, roger although, to be fair, it sounds like you have experience with CosmosDB, and I don't
Bluestopher
Bluestopher•7mo ago
The implementation does not matter.
JakenVeina
JakenVeina•7mo ago
so, ultimately, the data record we're storing here is...
public class ExampleDataRecord
{
public Guid Id { get; init; }
public string Name { get; set; }
}
public class ExampleDataRecord
{
public Guid Id { get; init; }
public string Name { get; set; }
}
Bluestopher
Bluestopher•7mo ago
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.
JakenVeina
JakenVeina•7mo ago
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?
Bluestopher
Bluestopher•7mo ago
Let's say the value for Name passed was "test".
JakenVeina
JakenVeina•7mo ago
what's the call/syntax for creating a record in your CosmosDB driver? I can illustrate what it would be for EF Core....
Bluestopher
Bluestopher•7mo ago
CreateAsync or something from the library. Sudo code works fine
JakenVeina
JakenVeina•7mo ago
[HttpPost(Name = "CreateRecord")]
public IActionResult Create(string name)
{
var @record = new ExampleDataRecord()
{
Id = Guid.NewGuid(),
Name = name
};

_context.Set<ExampleDataRecord>().Add(@record);

await _context.SaveChangesAsync();

return @record;
}
[HttpPost(Name = "CreateRecord")]
public IActionResult Create(string name)
{
var @record = new ExampleDataRecord()
{
Id = Guid.NewGuid(),
Name = name
};

_context.Set<ExampleDataRecord>().Add(@record);

await _context.SaveChangesAsync();

return @record;
}
Bluestopher
Bluestopher•7mo ago
Yeah, this is way off from what I am able to do.
JakenVeina
JakenVeina•7mo ago
why so?
Bluestopher
Bluestopher•7mo ago
I see the thought, but I cannot do it this way sadly. My implementation is for cosmosdb Also, it needs to be a service
JakenVeina
JakenVeina•7mo ago
so, move it to a service
Bluestopher
Bluestopher•7mo ago
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
JakenVeina
JakenVeina•7mo ago
public interface IExampleDataService
{
Task<ExampleDataRecord> CreateASync(string name);
}

public class ExampleDataService
: IExampleDataService
{
public async Task<ExampleDataRecord> CreateAsync(string name)
{
var @record = new ExampleDataRecord()
{
Id = Guid.NewGuid(),
Name = name
};

_context.Set<ExampleDataRecord>().Add(@record);

await _context.SaveChangesAsync();

return @record;
}
}
public interface IExampleDataService
{
Task<ExampleDataRecord> CreateASync(string name);
}

public class ExampleDataService
: IExampleDataService
{
public async Task<ExampleDataRecord> CreateAsync(string name)
{
var @record = new ExampleDataRecord()
{
Id = Guid.NewGuid(),
Name = name
};

_context.Set<ExampleDataRecord>().Add(@record);

await _context.SaveChangesAsync();

return @record;
}
}
Bluestopher
Bluestopher•7mo ago
Or specifying the data type is fine that works too, but I believe that would just be extension methods.
JakenVeina
JakenVeina•7mo ago
like I said, I'm not familiar with CosmosDB specifics what is different about interacting with the data store?
Bluestopher
Bluestopher•7mo ago
I don’t understand how to types to their implementation.
JakenVeina
JakenVeina•7mo ago
you don't have types an implementations your type IS the implementation it's data
Bluestopher
Bluestopher•7mo ago
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
JakenVeina
JakenVeina•7mo ago
yes, that's what you want you want to be explicit deduplicate code as you see fit, with extensions
Bluestopher
Bluestopher•7mo ago
Can I have shared functionality with an abstract or base implementation?
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
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.
JakenVeina
JakenVeina•7mo ago
do you mean "data" types?
Bluestopher
Bluestopher•7mo ago
Yeah, “data types”. Sorry on mobile right now and autocorrect kills me haha.
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
How would you make your data service generic above and map to specific implementations?
JakenVeina
JakenVeina•7mo ago
I still really have no idea what you mean by "specific implementations" give me an example
Bluestopher
Bluestopher•7mo ago
Yea, I’m seeing that now, but confused how I can implement it to be a generic service.
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
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.
JakenVeina
JakenVeina•7mo ago
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?
Bluestopher
Bluestopher•7mo ago
Yeah, exactly
JakenVeina
JakenVeina•7mo ago
okay so, you have an API that looks like....
[HttpPost(Name = "CreateRecord")]
public async Task<IActionResult> CreateAsync(??? requestData)
{

}
[HttpPost(Name = "CreateRecord")]
public async Task<IActionResult> CreateAsync(??? requestData)
{

}
how do you differentiate requestData here? this one endpoint is supposed to handle creation of all different types of data records?
Bluestopher
Bluestopher•7mo ago
Deserialize data to the correct type based on the caller
JakenVeina
JakenVeina•7mo ago
what do you mean by "based on the caller"?
Bluestopher
Bluestopher•7mo ago
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.
JakenVeina
JakenVeina•7mo ago
okay, so it's part of the request like, one of the JSON fields
Bluestopher
Bluestopher•7mo ago
Yeah, the request will contain an annotation on what to do.
JakenVeina
JakenVeina•7mo ago
so
Bluestopher
Bluestopher•7mo ago
Yup, exactly.
JakenVeina
JakenVeina•7mo ago
have you considered that you have re-invented URLs?
Bluestopher
Bluestopher•7mo ago
Oh my gosh I see the correlation
JakenVeina
JakenVeina•7mo ago
so instead of a request being....
PUT /api/records/new
{
type: "RecordA",
data: { ... }
}
PUT /api/records/new
{
type: "RecordA",
data: { ... }
}
PUT /api/a-records/new
{ ... }
PUT /api/a-records/new
{ ... }
in effect you let the framework do this type mapping for you so you write
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordACreationModel model)
{
return Ok(_recordAService.Create(model));
}
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordACreationModel model)
{
return Ok(_recordAService.Create(model));
}
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordBCreationModel model)
{
return Ok(_recordBService.Create(model));
}
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordBCreationModel model)
{
return Ok(_recordBService.Create(model));
}
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
Bluestopher
Bluestopher•7mo ago
But when I have 20 different data types I will have 20 different controllers. This is where the service comes in handy
JakenVeina
JakenVeina•7mo ago
to an extent what you might like is the CQRS pattern
Bluestopher
Bluestopher•7mo ago
I saw the command bus pattern
JakenVeina
JakenVeina•7mo ago
your "service" layer becomes something like....
Bluestopher
Bluestopher•7mo ago
Do you have any sample implementations you can recommend?
JakenVeina
JakenVeina•7mo ago
public class CreateRecordARequest
: IRequest<RecordACreationModel, RecordA>
{
public async Task<RecordA> Execute(RecordACreationModel model)
{
// Data access and business logic here
}
}
public class CreateRecordARequest
: IRequest<RecordACreationModel, RecordA>
{
public async Task<RecordA> Execute(RecordACreationModel model)
{
// Data access and business logic here
}
}
and the top layer becomes something like...
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordACreationModel model)
{
return _requestManager.HandleRequest<RecordACreationModel, RecordA>(model);
}
[HttpPut("new")]
public async Task<IActionResult> CreateAsync(RecordACreationModel model)
{
return _requestManager.HandleRequest<RecordACreationModel, RecordA>(model);
}
and again, the mapping is mainly done with an IoC container
Bluestopher
Bluestopher•7mo ago
I see. How do I map to multiple different IRequest?
JakenVeina
JakenVeina•7mo ago
MediatR is the big man in town, for CQRS in .NET
JakenVeina
JakenVeina•7mo ago
GitHub
GitHub - jbogard/MediatR: Simple, unambitious mediator implementati...
Simple, unambitious mediator implementation in .NET - GitHub - jbogard/MediatR: Simple, unambitious mediator implementation in .NET
Bluestopher
Bluestopher•7mo ago
Thanks. So, I write the executors and hook them up via DI with mediatR?
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
I see what you are saying
JakenVeina
JakenVeina•7mo ago
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
Bluestopher
Bluestopher•7mo ago
Should I just read through mediatr documentation or is there more I should read for how to implement it this way?
JakenVeina
JakenVeina•7mo ago
you can define a BasicCreateRequestHandler that does it the basic way, and reuse that for, say, 15 of your 20 different data types
Bluestopher
Bluestopher•7mo ago
I 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
JakenVeina
JakenVeina•7mo ago
and implement unique ones for the rest
Bluestopher
Bluestopher•7mo ago
I like that
JakenVeina
JakenVeina•7mo ago
the MediatR docs are definitely a good place to start
Bluestopher
Bluestopher•7mo ago
This is exactly what I need
JakenVeina
JakenVeina•7mo ago
on top of that, just google up CQRS and read whatever looks approachable to you I don't have any specific recommendations
Bluestopher
Bluestopher•7mo ago
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
JakenVeina
JakenVeina•7mo ago
o7
PixxelKick
PixxelKick•7mo ago
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
Bluestopher
Bluestopher•7mo ago
Do you have an example? I’m kind of confused
PixxelKick
PixxelKick•7mo ago
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>
Task<Expression<Func<T, bool>> CompileAsync(MyDbContext db, string userId);
Task<Expression<Func<T, bool>> CompileAsync(MyDbContext db, string userId);
IWriteRequest<T>
Task WriteAsync(MyDbContext db, string userId, T entity);
Task WriteAsync(MyDbContext db, string userId, T entity);
And then on FooReadResponse I have this static method:
public static Expression<Func<FooEntity, FooReadResponse> FromModel(MyDbContext db, string userId)
{
return entity => new() { .... };
}
public static Expression<Func<FooEntity, FooReadResponse> FromModel(MyDbContext db, string userId)
{
return entity => new() { .... };
}
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 does
Bluestopher
Bluestopher•7mo ago
I see what you mean. I’m going to give this a try at implementation for fixing my issue. Thanks for the feedback @PixxelKick.