The way I see it, there is no way to implement this in C# alone, because string literals are always System.Strings and because the C# type system does is completely oblivious to array sizes.
Assuming you go with a custom value type (and yes, you have to declare 10 char fields, because char[10] would be stored on the heap),
struct String10
{
char c0;
char c1;
...
char c9;
public String10(string literal){...}
}
You could write a tool (as a post-compilation step) that goes through the IL and rejects every invocation of that String10 constructor that doesn't have a valid (i.e. at most 10 characters) string literal as its parameter.
new String10("0123456789") //valid
new String10("0123456789A") //rejected
new String10(someString) //has to be rejected as well → undecidable ↔ halting problem
If you don't like having to write new String10(...), you could define an implicit conversion from System.String to String10 instead. Under the hoods, this would be a static method called by the C# compiler in your stead.
One library that allows you to look at IL is mono.cecil.
You will get a new data type, that is distinct from System.String. You can override the ToString method so that String10 can be used in String.Format and friends, you could even define a widening (implicit) conversion to System.String so that you can use String10 with APIs that expect a System.String.