Receiving error about nullable type parameter even when parameter has notnull constraint

I think this issue is very similar to what is happening in this post.

Note that a T? where T : class and a T? where T : struct are represented very differently in the CLR. The former is just the CLR type T. There are not separate types in the CLR to differentiate between T and T?. T? in C# just adds extra compile time checking by the C# compiler. On the other hand, The latter is represented by the CLR type Nullable<T>.

So let's consider your method:

T? Read (Guid id);

How should this be represented in the CLR? What is the return type? The compiler don't know whether T is a reference type or a value type, so the compiler cannot decide whether the method signature should be:

T Read (Guid id);

or:

Nullable<T> Read (Guid id);

The same error is raised if you don't use the notnull constraint. You need to specify what that type is with a class or struct constraint. You don't need to specify notnull as structs were always nullable and, with nullable ref types enabled, so are classes.

Just add where T:class or where T:struct.

Reference Types

If you add the class constraint, eg:

#nullable enable

interface IDataAdapter<T>       
    where T:class
{
    T? Read (Guid id); // error CS8627
    
    void Something(T input);
}

class StringAdapter:IDataAdapter<string>
{
    public string Read(Guid id)=>id.ToString();
    
    public void Something(string input){}
}

The following call will generate a warning:

var adp=new StringAdapter();
string? x=null;
adp.Something(x);  //CS8604: Possible null reference argument ....

Value Types

Using struct to create an IntAdapter on the other hand results in a compilation error if the argument is nullable :

interface IDataAdapter<T>       
    where T:struct
{
    T? Read (Guid id); // error CS8627
    
    void Something(T input);
}


class IntAdapter:IDataAdapter<int>
{
    public int? Read(Guid id)=>id.ToString().Length;
    
    public void Something(int input){}
}

void Main()
{
    
    var adp=new IntAdapter();
    int? x=null;
    adp.Something(x);  //CS1503: Cannot convert from int? to int
}

That's because the compile generated methods that expect an int? instead of an int.

Explanation

The reason is that the compiler has to generate very different code in each case. For a class, it doesn't have to do anything special. For a struct, it has to generate a Nullable< T>.

This is explained in the The issue with T? section in Try out Nullable Reference Types :

This distinction between nullable value types and nullable reference types comes up in a pattern such as this:

void M<T>(T? t) where T: notnull

This would mean that the parameter is the nullable version of T, and T is constrained to be notnull. If T were a string, then the actual signature of M would be M([NullableAttribute] T t), but if T were an int, then M would be M(Nullable t). These two signatures are fundamentally different, and this difference is not reconcilable.

Because of this issue between the concrete representations of nullable reference types and nullable value types, any use of T? must also require you to constrain the T to be either class or struct.