0

Pretty straightforward question. I want to allow only DTO models in inputs (class & record types).

If I leave the class constraint only, I'm still able to put string in there. I want to not allow the primitive types.

If I put class, new(), it doesn't allow string just as expected, but I'm not allowed to use positional records either which is a problem.

public class Asd
{
    public string Name { get; set; }
}

public record Asd2(string Name);

public Task DoSomethingAsync<TInput>(TInput data) where TInput : class
{
    var message = JsonSerializer.Serialize(data); // if TInput is a string, it results into faulty \u0022 codes
}

Edit: The actual issue is that I have JsonSerializer.Serialize(data) inside that method and if a string is passed accidentally, it will result in something like:

"{\u0022Name\u0022:\u0022Test\u0022}"

nop
  • 4,711
  • 6
  • 32
  • 93
  • 2
    Unless you use a common interface, how should the compiler know if a class is a 'DTO' or not? I would assume you have custom non-DTO classes that you also want to disallow. – JonasH Apr 06 '22 at 06:14
  • FYI `string` is technically a class and not a primitive type, it just behaves like one. You'll have to create a base type (in your case an interface) and then implement/ inherit that type on all your DTO classes, something like `IAsd` then set your generic constraint to `where TInput : IAsd`, as JonasH said you probably also have other custom non-DTO classes you want to disallow. More on C#'s string and it not being a primitive type [here](https://stackoverflow.com/q/3965752/9363973) – MindSwipe Apr 06 '22 at 06:17
  • In that case, can I just disable `string` because it invalidates my code – nop Apr 06 '22 at 06:18
  • This is actually unclear - what do you consider a "DTO"? Why not use a struct or record struct as a DTO? `it invalidates my code` how? The core right now knows nothing about the type so it can only work with reflection or unsafe casts, defeating the use of generics – Panagiotis Kanavos Apr 06 '22 at 06:19
  • @PanagiotisKanavos, `Asd` and `Asd2` types only. – nop Apr 06 '22 at 06:21
  • [Here](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters)'s a list of all possible type constraints, there is no `where TypeA is not TypeB` – MindSwipe Apr 06 '22 at 06:22
  • 1
    If you want only two types, use an interface – Panagiotis Kanavos Apr 06 '22 at 06:22
  • @PanagiotisKanavos, there is a `JsonSerializer.Serialize(...)` in that method and if I pass a `string`, it's not going to work, which means I have to constraint it. – nop Apr 06 '22 at 06:23
  • So use an interface, maybe with some pattern matching. `Point` wouldn't work in your case either although it's neither a primitive type or a struct. Post your actual code if you want people to help – Panagiotis Kanavos Apr 06 '22 at 06:24
  • If you have no control over the types you can create wrappers, perhaps even just one, and convert between them with an implicit cast operator. You'll have to post your code and explain the actual problem, not what you think the solution is. – Panagiotis Kanavos Apr 06 '22 at 06:28
  • `it results into faulty \u0022 codes` not really, although it may not be what you expected. That's the escape code for a quote - that's just how your tooling displays quotes inside a string. `Serialize("potato")` results in a perfectly valid JSON string containing a Javascript string, ie with quotes: `"potato"`. Visual Studio's Watch window would display this as `"\"potato\""` because `""potato""` isn't a valid string. If you wanted to assign a variable with such a string you'd have to write `var json="\"potato\"";` – Panagiotis Kanavos Apr 06 '22 at 06:31
  • @PanagiotisKanavos, yes that's the issue. Is there a way to easily remove the quotes between `{` and `}` and I can just do `if (data is typeof(string) { throw new ... }` – nop Apr 06 '22 at 06:37
  • I do not think it is feasible to expect your serialization library to handle any kind of arbitrary objects. In the end it will fall to the developer to only serialize compatible types. I would suggest writing more unit tests if you have issues, that is also a good way to learn about what can be serialized or not. – JonasH Apr 06 '22 at 06:38
  • 1
    @nop that's not an issue. That's perfectly valid JSON produced from `Asd` or `Asd2`. What you posted is actually `{"Name":"Test"}`. `\u0022` is the escape sequence for the double quote, `"`. I'ts perfectly valid to use the escape sequence, although I'm certain that System.Text.Json *doesn't* produce an escape sequence. That's how your tooling displays the double quote inside the string. Why do you assume there's any kind of problem? – Panagiotis Kanavos Apr 06 '22 at 06:45

2 Answers2

4

You need to have some kind of commonality for the restriction. This needs to be a common base class or interface.

As you want to use Records as well, then this must be an interface. But you can just define a simple, empty one if you want just for this purpose.

So you define a simple empty interface (better is if you actually have some common functions/properties).

public interface MyInterface
{

}

So then you code looks like this

public Task DoSomethingAsync<TInput>(TInput data) where TInput : MyInterface
{
}

And all your classes and records inherit from it like this.

public record AsdRecord: MyInterface
{
       //details here
}

public class AsdClass: MyInterface
{
        //details here
}
jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
  • Thanks for the answer! This is however not an option to me, because these models are into an SDK and I have no access to them. In that case, is it possible to just forbid `string` usage? – nop Apr 06 '22 at 06:20
  • 1
    You can create wrapper type(s) with implicit cast operators – Panagiotis Kanavos Apr 06 '22 at 06:26
  • That was kind of important information which you left out! The problem is, even if you could restrict Strings, what about other data types or object types? The list is endless. Best to create a wrapper class, as suggested. – jason.kaisersmith Apr 06 '22 at 06:27
3

If your list of allowed types can't be formulated with a type constraint, consider creating a list of allowed types (either hardcoded in code or with reflection at runtime e.g. on the namespace) and test against that list at runtime right at the beginning of the method (like you also test incoming parameters). Maybe something like this

private static readonly HashSet<Type> AllowedTypes = new HashSet<Type>
{
    typeof(MyTypeA),
    typeof(MyTypeB),
};

public Task DoSomethingAsync<TInput>(TInput data)
{
    if(!AllowedTypes.Contains(typeof(TInput)))
        throw new ArgumentException($"The type {typeof(TInput).Name} can' t be used here.");

    // ...
}

While this doesn't help you at compile time, at least it helps you at runtime.

Oliver
  • 43,366
  • 8
  • 94
  • 151
  • It would be faster to use pattern matching. This tries to solve a non-problem though - serializing a JSON string to JSON produces a valid string. Nothing in the question shows an actual problem – Panagiotis Kanavos Apr 06 '22 at 06:46
  • @PanagiotisKanavos, by pattern matching you mean to remove the quotes in the beginning and at the end if `data is string`? Otherwise I understand what you meant in your comments – nop Apr 06 '22 at 06:52
  • @PanagiotisKanavos I think you're right from the pure technical point of view. I just tried to give an example on how to restrict generic types if type constraints don't match your needs. Personally my generic methods either work with any type or I define some constraint on an interface or a base class and never needed such an approach (like jason said in his answer), but if you really can't do that, this would be my *last bullet*. – Oliver Apr 06 '22 at 06:52
  • @PanagiotisKanavos I'm not sure if pattern matching is really *faster*. Using the hash set makes the lookup O(1) and AFAIK pattern matching will done in IL as an if-elseif-else approach, which makes it O(n). Nevertheless, if you have to do something with the specific type, it is much better readable and maintainable to use pattern matching instead of the hash set. – Oliver Apr 06 '22 at 06:56