C
C#•2mo ago
Neophyte

How to to dynamically provide T type of a generic method

I am having an interface declaration which has many-many implementations. The usecase/logic is similar, but the return type differs. (tldr: the services parse and validate data extracted from different systems. They each return the parsed structure relevant to the given system and the detected errors). I want to achieve something like this, however I cannot pass the Type retrieved from the specific implementation in the method call. The services are provided through factory pattern. Is it even possible what I want to achieve? What better solutions/patterns are there to meet my requirements?
No description
59 Replies
Angius
Angius•2mo ago
You can't Not without reflections
Neophyte
NeophyteOP•2mo ago
the alternative I was thinking on was to create some sort of if structure and cast directly to the specific interface and remove these methods from the interface declarations how would I do that with reflections? like defining it as object and do type analysis in the implementation?
canton7
canton7•2mo ago
Your options there are reflection, a type switch, dynamic, and the visitor pattern
Angius
Angius•2mo ago
Something like
MethodInfo method = typeof(MyImplementation).GetMethod(nameof(MyImplementation.MyTypeSpecificMethod));
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(instance, null);
MethodInfo method = typeof(MyImplementation).GetMethod(nameof(MyImplementation.MyTypeSpecificMethod));
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(instance, null);
Word of warning: reflections are slow You'll want to cache all that if you choose to use it
Neophyte
NeophyteOP•2mo ago
generic.Invoke(this, null); --> what this would refer here to?
Angius
Angius•2mo ago
Ah, that's the instance you want to call the method on So an instance of MyImplementation
Neophyte
NeophyteOP•2mo ago
thanks, I will give it a look
ero
ero•2mo ago
this might be a naive question, but you can't just make the interface generic, can you?
Neophyte
NeophyteOP•2mo ago
thanks, I would want to avoid using dynamics 🙂 Visitor pattern might work as well. Havn't used it yet, but will check this also. I am not quite sure though if I get what your concept of type switch was?
canton7
canton7•2mo ago
switch (service)
{
case MyImplementation x:
service.MyTypeSpecificMethod(MyImplementation);
break;
}
switch (service)
{
case MyImplementation x:
service.MyTypeSpecificMethod(MyImplementation);
break;
}
and so on What's your actual use-case here?
Neophyte
NeophyteOP•2mo ago
I fail to see how it would differ vs current solution. How will I be able to pass the type as a generic? I am already receiving a system specific implementation that does the logic. But on the caller side, I would want to avoid create a switch case that will handle each implementation and provide a specific type. Thus the GetMyType methods or I miss something
canton7
canton7•2mo ago
Nope, that's about it. I'm just listing out the 4 options for dealing with this general class of problem
ero
ero•2mo ago
maybe i'm missing something as well, but why can the interface not be generic?
canton7
canton7•2mo ago
An actual concrete use-case would be helpful. In your toy example, MyImplementation already knows the concrete type of MyType, so it doesn't need that to be passed into MyTypeSpecificMethod
Neophyte
NeophyteOP•2mo ago
Web API I have IMyInterface, with many implementations. I am using factory pattern to get the specific implementation. The user selects a system type and uploads a file for that specific system type. There are two methods to be called. 1) parse the Stream into the IList<SystemDto> (system specific structure) 2) validate the IList<SystemDto> if they are valid and return the specific errors, if any.
canton7
canton7•2mo ago
SystemDto is implementation-specific? I'm not sure what you mean by "system specific"
Neophyte
NeophyteOP•2mo ago
yes
canton7
canton7•2mo ago
So, where does this generic type parameter come in?
Neophyte
NeophyteOP•2mo ago
I think this is only possible if the interface gets generic, as @ero suggested
canton7
canton7•2mo ago
Having IMyInterface<T>, and a factory which can return different Ts depending on some parameter, doesn't work: the factory needs to return a single type Can you sketch out some of the actual types and methods involved, so we can talk specifics?
Neophyte
NeophyteOP•2mo ago
I will try, need some time for that
ero
ero•2mo ago
well, this is solvable, but it's not pretty...
interface II
{
object M();
}

interface II<T>
{
new T M();
object II.M() => M();
}
interface II
{
object M();
}

interface II<T>
{
new T M();
object II.M() => M();
}
canton7
canton7•2mo ago
I'm assuming it's something like:
interface IUploadHandler
{
List<SystemDto> HandleUpload(Stream stream);
List<Error> Validate(List<SystemDto> dtos);
}

class UploadHandlerFactory
{
public IUploadHandler CreateHandler(string uploadType) { ... }
}

class PdfUploadHandler : IUploadHandler
{
...
}
interface IUploadHandler
{
List<SystemDto> HandleUpload(Stream stream);
List<Error> Validate(List<SystemDto> dtos);
}

class UploadHandlerFactory
{
public IUploadHandler CreateHandler(string uploadType) { ... }
}

class PdfUploadHandler : IUploadHandler
{
...
}
... but I don't see why any of those methods need to be generic That doesn't help? In my example, CreateHandler has to return a non-generic II, which means that nothing uses II<T>
ero
ero•2mo ago
i know :) was just commenting on "the factory needs to return a single type", which would be II in my example
canton7
canton7•2mo ago
The point is more, since the code which uses II knows nothing of T, then II<T> is never used by anyone
ero
ero•2mo ago
well it would be by the impls
canton7
canton7•2mo ago
Nothing can call T M(), since nothing knows what T is to store the return type
Neophyte
NeophyteOP•2mo ago
public class Result<T> : Result
{
public T Value { get; set; }
}
public class Result<T> : Result
{
public T Value { get; set; }
}
enum FileType
{
UNKNOWN,
CSV,
JSON
}
enum FileType
{
UNKNOWN,
CSV,
JSON
}
enum SystemType
{
AZURE,
AWS
}
enum SystemType
{
AZURE,
AWS
}
interface IDataLoadingInterface
{
Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType);
Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers);
}
interface IDataLoadingInterface
{
Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType);
Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers);
}
class AzureDataLoadingService : IDataLoadingInterface
{
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType)
{
throw new NotImplementedException();
}

public Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers)
{
throw new NotImplementedException();
}
}
class AzureDataLoadingService : IDataLoadingInterface
{
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType)
{
throw new NotImplementedException();
}

public Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers)
{
throw new NotImplementedException();
}
}
class AwsDataLoadingService : IDataLoadingInterface
{
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType)
{
throw new NotImplementedException();
}

public Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers)
{
throw new NotImplementedException();
}
}
class AwsDataLoadingService : IDataLoadingInterface
{
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType)
{
throw new NotImplementedException();
}

public Result<IList<TTwo>> ValidateSystemDtos<TOne, TTwo>(IList<TOne> systemDtos, string[] headers)
{
throw new NotImplementedException();
}
}
class DataLoadingInterfaceFactory
{
public static IDataLoadingInterface CreateDataLoadingInterface(SystemType systemType)
{
switch (systemType)
{
case SystemType.AZURE:
return new AzureDataLoadingService();
case SystemType.AWS:
return new AwsDataLoadingService();
default:
throw new NotImplementedException();
}
}
}
class DataLoadingInterfaceFactory
{
public static IDataLoadingInterface CreateDataLoadingInterface(SystemType systemType)
{
switch (systemType)
{
case SystemType.AZURE:
return new AzureDataLoadingService();
case SystemType.AWS:
return new AwsDataLoadingService();
default:
throw new NotImplementedException();
}
}
}
canton7
canton7•2mo ago
Why is AzureDataLoadingService.ParseStreamIntoSystemDto generic? Can it really handle any sort of T? What does the code which calls IDataLoadingInterface.ParseStreamIntoSystemDto look like? That must be a generic method, because it needs to create a variable of type Result<IList<T>>?
Neophyte
NeophyteOP•2mo ago
in the generic methods I do a typecheck
if (GetSystemDtoType() != typeof(T))
{
logger.Warn("Invalid type. Expected: {expectedType}, actual: {actualType}", GetSystemDtoType().Name, typeof(T).Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}
if (GetSystemDtoType() != typeof(T))
{
logger.Warn("Invalid type. Expected: {expectedType}, actual: {actualType}", GetSystemDtoType().Name, typeof(T).Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType) where T : class, new()
{
var result = new Result<IList<T>>();

if (GetSystemDtoType() != typeof(T))
{
logger.Warn("Invalid type. Expected: {expectedType}, actual: {actualType}", GetSystemDtoType().Name, typeof(T).Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}

// TODO: create an interface for Csv and Json parser services and retrieve the proper one via some sort of factory, The return object shall be the same for both services
if (FileType.Csv == fileType)
{
var parsingResult = AzureCsvDataParserService.ParseStreamIntoHrCsvData(stream);
if (!parsingResult.IsSuccessful)
{
result.CopyErrorsFrom(parsingResult);
return result;
}

var conversionResult = AzureCsvConverter.ConvertDtosToEntries(parsingResult.HrCsvEntries);
if (!conversionResult.IsSuccessful)
{
result.CopyErrorsFrom(conversionResult);
return result;
}

if (GetSystemDtoType() != conversionResult.Value.GetType())
{
logger.Warn("Parsing and conversion resulted in different type vs what is contracted in {HrCsvDataLoadingInterface}.{GetSystemDtoType}. Expected: {expectedType}, actual: {actualType}", nameof(HrCsvDataLoadingInterface), nameof(GetSystemDtoType), GetSystemDtoType().Name, conversionResult.Value.GetType().Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}

result.Value = conversionResult.Value as IList<T>;
}
else if (FileType.Json == fileType)
{
}
else
{
result.Errors.Add(new ValidationError("Invalid file type received during Azure data upload"));
return result;
}

return result;
}
public Result<IList<T>> ParseStreamIntoSystemDto<T>(Stream stream, FileType fileType) where T : class, new()
{
var result = new Result<IList<T>>();

if (GetSystemDtoType() != typeof(T))
{
logger.Warn("Invalid type. Expected: {expectedType}, actual: {actualType}", GetSystemDtoType().Name, typeof(T).Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}

// TODO: create an interface for Csv and Json parser services and retrieve the proper one via some sort of factory, The return object shall be the same for both services
if (FileType.Csv == fileType)
{
var parsingResult = AzureCsvDataParserService.ParseStreamIntoHrCsvData(stream);
if (!parsingResult.IsSuccessful)
{
result.CopyErrorsFrom(parsingResult);
return result;
}

var conversionResult = AzureCsvConverter.ConvertDtosToEntries(parsingResult.HrCsvEntries);
if (!conversionResult.IsSuccessful)
{
result.CopyErrorsFrom(conversionResult);
return result;
}

if (GetSystemDtoType() != conversionResult.Value.GetType())
{
logger.Warn("Parsing and conversion resulted in different type vs what is contracted in {HrCsvDataLoadingInterface}.{GetSystemDtoType}. Expected: {expectedType}, actual: {actualType}", nameof(HrCsvDataLoadingInterface), nameof(GetSystemDtoType), GetSystemDtoType().Name, conversionResult.Value.GetType().Name);

result.Errors.Add(new ValidationError("Invalid type during parsing. Please contact support!"));
return result;
}

result.Value = conversionResult.Value as IList<T>;
}
else if (FileType.Json == fileType)
{
}
else
{
result.Errors.Add(new ValidationError("Invalid file type received during Azure data upload"));
return result;
}

return result;
}
canton7
canton7•2mo ago
I rather suspect, that the problem is that the code which usesIDataLoadingInterface needs to be non-generic. I can't see any way that it can be generic. Therefore you need to have IDataLoadingInterface.ParseStreamIntoSystemDto be non-generic: it needs to return a Result<IList<ISomeBaseDtoType>>, rather than an implementation-specific DTO type. That doesn't lose you any type-safety: you've still got a runtime check in AzureDataLoadingService.ParseStreamIntoSystemDto, expect now you're testing against the runtime type of ISomeBaseDtoType, and not a generic type parameter And now that you've removed generics, you don't need all that new T() stuff: you can just create a new AzureDataDto() directly Your real problem is that the code which calls IDataLoadingInterface.ParseStreamIntoSystemDto can't create a variable to hold the value returned from it. Because in your design, the type of the return value is generic, but the generic isn't known by that code The only solution is to make the return type non-generic. Reflection doesn't help: that just stores the return value in an object, which you could just do without reflection by making the method non-generic and making it return object 😛
Neophyte
NeophyteOP•2mo ago
Indeed, however given I cannot enforce some common structure accross each system, I couldn't abstract it a satisfying level. If I understand you correctly this ISomeBaseDtoType interface would be only to *trick *the generic part, so the IDataLoadingInterface method's doesn't need to be generic? Thus the interface would look like pretty blank. interface ISomeBaseDtoType { } Though I assume this wouldn't make a big difference as I would need to cast it to a specific type anyway, where I again cannot really use the Type retrieved from the methods.
canton7
canton7•2mo ago
If I understand you correctly this ISomeBaseDtoType interface would be only to trickthe generic part, so the IDataLoadingInterface method's doesn't need to be generic?
Just a common concrete base type of all of the DTOs, which the calling code can know at compile-time. Make it a marker interface, or just object, if you want
Though I assume this wouldn't make a big difference as I would need to cast it to a specific type anyway, where I again cannot really use the Type retrieved from the methods.
In the calling (non-generic) code? Yeah you can't access any of the properties of the DTO, because the DTO could be anything. You already have that problem, but you're trying to throw generics at it to try and solve it, but that doesn't help here The real OO way around this is to give the SystemDto responsibility for validating itself. That way you don't need the shenanigans with having to pass the DTO (which could be any type) to the handler which expects exactly that type. But given that you've called it a DTO, I'm assuming it's crossing some boundary which means that's not feasible?
Neophyte
NeophyteOP•2mo ago
That could work, however this is an entity and a dto in place. I would love to avoid building logic into them and that still wouldn't solve to return the system specific errors
canton7
canton7•2mo ago
What do you mean by "return the system specific errors"?
Neophyte
NeophyteOP•2mo ago
as my issue is indeed that the return type can change, depending on the system that I cannot really enforce common structure on the entries
canton7
canton7•2mo ago
You've got a common error type already, no? With Result<T> / ValidationError
Neophyte
NeophyteOP•2mo ago
I understand it can be misleading in above example, that error does not hold the specific data. The Error you took is to handle errors during the method call. But a failed validation can still be a successful call.
public class Error
{
public string Title { get; set; }
public string Description { get; set; }
public int ErrorCode { get; set; }
}
public class Error
{
public string Title { get; set; }
public string Description { get; set; }
public int ErrorCode { get; set; }
}
The systems specific error would be:
public class ValidatedHrCsvData : HrCsvData
{
public IList<HrCsvError> Errors { get; set; } = new List<HrCsvError>();
}
public class ValidatedHrCsvData : HrCsvData
{
public IList<HrCsvError> Errors { get; set; } = new List<HrCsvError>();
}
public class HrCsvError
{
public string ErrorMessage { get; set; }
public PropertyInfo Property { get; set; }
public string OriginalValue { get; set; }
}
public class HrCsvError
{
public string ErrorMessage { get; set; }
public PropertyInfo Property { get; set; }
public string OriginalValue { get; set; }
}
In order to ensure that the given entry of the uploaded data is clearly linked with the error (and the corrections used later on), the error is linked to specific parsed entry. the data is not stored yet anywhere, thus I don't have any Id reference to rely on..
canton7
canton7•2mo ago
I'm afraid I didn't follow that. What's "Hr"? What returns a ValidatedHrCsvData?
Neophyte
NeophyteOP•2mo ago
the ValidatedHrCsvData here extends the SystemDto replace any HR with Azure. by accident I have just taken a different example
canton7
canton7•2mo ago
Your code doesn't create or use a SystemDto anywhere, either?
Neophyte
NeophyteOP•2mo ago
here in ConvertDtosToEntries() (method name is to be fixed though)
canton7
canton7•2mo ago
And ValidatedHrCsvData is a member of SystemDto?
Neophyte
NeophyteOP•2mo ago
HrCsvData is a SystemDto. It s the system dto for HR systems uploaded from CSV
canton7
canton7•2mo ago
So SystemDto isn't actually a type? In that case, isn't this exactly the problem we've been discussing already? If your IDataLoadingInterface.ParseStreamIntoSystemDto can return literally any type, with no constraints, then of course the calling code can't access that type in any meaningful way
Neophyte
NeophyteOP•2mo ago
no, its an example name I used here so we ain't limited by the system. but there are AwsSystemDto, AzureSystemDto, HrCsvData (HrSystemDto) Thus I have created a new contract in the Interface Type GetMyType() so it would return the specific type but I understand this cannot work
canton7
canton7•2mo ago
Yep, because that Type is only known at runtime, but to be able to write code which uses a type, you need to know that type at compile-time I think you've been designing this entirely from the point of view of the data loading services. Switch it around, and figure out what the API should look like from the code which uses those services If that code needs to access error information, figure out how you can present that information in a way that code which doesn't know anything about Azure vs HR can access it (and you might need to do some encapsulation, where your error info DTO only presents basic info, but can write out more complex info to CSV/JSON/a logger/etc if asked)
Neophyte
NeophyteOP•2mo ago
ok, thanks! it was an educative discussion, I appreciate it!
canton7
canton7•2mo ago
Glad I could help a bit! But really, while the service / DTO split is nice and all, encapsulation is really the only neat solution in cases like this E.g. in a UI framework, you might have Button and TextBox which inherit from Control. Control exposes the general control-related stuff, but the only code which knows how to lay out and render a button is inside the Button class: that code can be called from anywhere, because the Layout and Render methods are on Control, and are non-generic, etc. You don't have a separate ButtonRenderer service and ButtonData DTO
ero
ero•2mo ago
i'm just spitballing, so sorry if this derails the discussion, but static abstract won't help here, right?
canton7
canton7•2mo ago
No The problem is that the caller does not know T, and cannot know T
ero
ero•2mo ago
who's the caller again?
canton7
canton7•2mo ago
The code which takes a Stream and a file type, gets the right sort of service to deal with that file type, gives it the stream, and gets back a set of DTOs which are specific to that file type I.e. the code which uses all of the code snippets pasted above
ero
ero•2mo ago
mhh... i'm hijacking this thread because i'm interested in this now and need my brain to understand it. so, an interface like
interface II<T> where T : II<T> {
static abstract Result<T> ParseFromCsv(Stream stream);
}
interface II<T> where T : II<T> {
static abstract Result<T> ParseFromCsv(Stream stream);
}
won't work, because we have, say, AzureCsvDataParser and AwsCsvDataParser? well i mean... i think you could make it work if IDataLoadingInterface were generic, maybe...? eh, what do i know
canton7
canton7•2mo ago
The problem is, if that interface is generic, how does the caller provide the right T?
ero
ero•2mo ago
i mean i have like a messed up idea, but since i'm not aware of the full context i'm afraid even that would fall flat...
interface ICsvParser
{
static abstract Result<IList<T>> Parse<T>(Stream stream);
}

interface ICsvConverter
{
static abstract Result<IList<TTo>> Convert<TFrom, TTo>(IList<TFrom> values);
}

interface IDataLoader
{
Result<IList<T>> Parse<T>(Stream stream, FileType type);
Result<IList<TError>> Validate<T, TError>(IList<T> values);
}

abstract class DataLoader<TParser, TConverter> : IDataLoader
where TParser : ICsvParser
where TConverter : ICsvConverter
{
// di logger i guess? could make it abstract and set it in inheriting classes...

public Result<IList<T>> Parse<T>(Stream stream, FileType type)
{
if (type == FileType.Csv)
{
var parseRes = TParser.Parse(stream);
// check, log

var convRes = TConverter.Convert(parseRes.Entries);
// check, log
}
else // ...
}
}
interface ICsvParser
{
static abstract Result<IList<T>> Parse<T>(Stream stream);
}

interface ICsvConverter
{
static abstract Result<IList<TTo>> Convert<TFrom, TTo>(IList<TFrom> values);
}

interface IDataLoader
{
Result<IList<T>> Parse<T>(Stream stream, FileType type);
Result<IList<TError>> Validate<T, TError>(IList<T> values);
}

abstract class DataLoader<TParser, TConverter> : IDataLoader
where TParser : ICsvParser
where TConverter : ICsvConverter
{
// di logger i guess? could make it abstract and set it in inheriting classes...

public Result<IList<T>> Parse<T>(Stream stream, FileType type)
{
if (type == FileType.Csv)
{
var parseRes = TParser.Parse(stream);
// check, log

var convRes = TConverter.Convert(parseRes.Entries);
// check, log
}
else // ...
}
}
class AzureCsvParser : ICsvParser;
class AzureCsvConverter : ICsvConverter;

class AzureDataLoader : DataLoader<AzureCsvParser, AzureCsvConverter>
{
// logger stuff, maybe
}
class AzureCsvParser : ICsvParser;
class AzureCsvConverter : ICsvConverter;

class AzureDataLoader : DataLoader<AzureCsvParser, AzureCsvConverter>
{
// logger stuff, maybe
}
humor me this kinda falls apart the instant you need to add more formats
Neophyte
NeophyteOP•2mo ago
maybe adding IParserFactory instead of the specific TParser could help. At the end, the parser will have to do the same. Parse<T>(Stream stream) no matter the implementation. This could reducde the number of generics needed. Same with TConverter yet this still wouldn't fix the generic return type problem. In above example you have used TError, but it is not in the method signature. So it must be a specific type unless you abstract the errors to a common structure, disregarding the systemType
ero
ero•2mo ago
it's a mistake. it is in your signature
Neophyte
NeophyteOP•2mo ago
the one calling IDataInterface.Validate<T, TError>(values) won't know compiletime the type of TError. Since he the specific implementation of IDataInterface is resolved only during runtime that's what I understood from above discussion I am thinking on establishing an abstraction that will grant the errors are on a common structure. like serializing into a json, or stg. similar that way the return type can be set in the interface without the need of a generic solution maybe this would reach the desired result with the least effort while not compromising too much on the standards
Want results from more Discord servers?
Add your server