-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal] Consider optional support for class
vs. struct
#46
Comments
Hi @prlcutting, yes, it's definitely something I've considered🙂 It's just not entirely clear to me that it's worth the hassle, when you can do something similar with record types these days, e.g. public record OrderId(int Value); This is something Thomas Levesque discusses on his blog, though things are even easier with c#10 as you can add
If the record approach doesn't work for you, I'd be interested to know why. I'm not opposed to adding it as an option, but just want to make sure it makes sense! |
Thanks for getting back to me @andrewlock, and apologies for the delayed response. Vladamir has an excellent blog post, C# 9 Records as DDD Value Objects which compares and contrasts C#'s records with a more traditional DDD The ability to compare identifiers through an You can of course achieve such protection of invariants with records, but now you're back to writing somewhat verbose, repetitive code, which obviates the benefits over traditional classes at that point. Avoiding that verbose, repetitive code could be aided by refactoring into a base class as you suggest, or generated automatically with a source generator. It would of course assume that the code generation could be configured (perhaps with additional attribute "options") to configure min/max bounds etc. Your challenge on my proposal made me think about this a little more deeply, and I'm grateful for that! Perhaps at this point, in the context of strongly typed identifiers, the question is more generalized to: which is better, a class hierarchy with most logic refactored into a shared base class, or source generation? I'd be curious if you have any opinions on that. Thanks again. |
One reason to choose your library over the record class and record struct is all the converters and interfaces that are included and generated. If it would be possible to use them with records, it would be nice. Considering all the debugging and syntax support they are getting/ going to get. But this is definitely something I can live without. [StronglyTypedId( StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter)]
public partial record struct MyId(Int16 Value); [StronglyTypedId( StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter)]
public partial record struct CompositId(string Part1, string Part2); This is nice-to-haves, affects few categories. Looking good but not worth the effort. |
As for me, this is a significant issue with this otherwise just awesome library, that reference type ids can be generated. Main issue is invariants protection: too easy to just pass or accidently have It would be much more preferable if it was C# Records are not an equivalent replacement to this library, because of all the nice features that are coming with code generation, e.g. type converters, static |
Ive also encountered this problem with the struct default constructor behaviour. I wonder if a simple public readonly record struct ProductsId
{
[Obsolete("Cannot be empty", true)]
public ProductsId() { }
public ProductsId(string value)
{
ArgumentNullException.ThrowIfNullOrEmpty(value);
//other validation
this.Value = value;
}
public string Value { get; }
} Which then looks like this at compile time And in saying this, if its possible to add this to the SourceGenerator code for this library? Maybe its already possible with the custom templates feature.
|
@andrewlock Reviving this. The new templating is great, but the generator code explicitly looks for structs only. |
FYI, I'm still torn on this one. Fundamentally I don't really want to use classes for this personally, plus it makes the templating some more complex if you want to create general templates, but I thought I'd take a look. Unfortunately, I ran into one specific blocker - if you use a Specifically, this method, in the private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId> Create_ClassGuidId(global::System.Text.Json.JsonSerializerOptions options)
{
if (!TryGetTypeInfoForRuntimeCustomConverter<global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId>(options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId> jsonTypeInfo))
{
var objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId>
{
// 👇 Doesn't compile ❌
ObjectCreator = () => new global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId(),
ObjectWithParameterizedConstructorCreator = null,
PropertyMetadataInitializer = _ => ClassGuidIdPropInit(options),
ConstructorParameterMetadataInitializer = null,
ConstructorAttributeProviderFactory = static () => typeof(global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId).GetConstructor(InstanceMemberBindingFlags, binder: null, global::System.Array.Empty<global::System.Type>(), modifiers: null),
SerializeHandler = ClassGuidIdSerializeHandler,
};
jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::StronglyTypedIds.IntegrationTests.Types.ClassGuidId>(options, objectInfo);
jsonTypeInfo.NumberHandling = null;
}
jsonTypeInfo.OriginatingResolver = this;
return jsonTypeInfo;
} I assume there's some configuration we could add to the template to fix this, but I'm not sure whta it is right now 🤔 I've tried using the Anyone have any ideas? |
Hi Andrew. In your excellent series of blog posts on Strongly Typed Id's, you make mention of some downsides to using a
struct
to model identity versus aclass
, due to implicit parameterless constructors. You also cite Vladamir Khorikov's article on the subject who makes further arguments for not choosing astruct
for modelling identity and favoring aclass
. Classes have their downsides too of course, not least of which is having tonull
-check them all over the place, so they're not a panacea, but if you're trying to follow DDD best practices where protection of invariants is paramount, aclass
-based option would be desirable.So, with all that said, have you considered enabling
class
-based strongly typed identifiers in your library in addition tostruct
-based ones?The text was updated successfully, but these errors were encountered: