Reputation: 23
I am writing a library similar to Fluent Validator that will allow object modification. For ordinary classes, it works without a problem but I try to code a similar approach with a record and stumble upon a big problem.
Rules are defined same as in Fluent Validator:
RuleFor(x => x.Name).ForceUppercaseFormat();
Method ForceUppercaseFormat()
is defined in extension class that looks like this:
public static ImmutableLogicRuleBuilder<TModel,string> ForceUppercaseFormat<TModel>(this ImmutableLogicRuleBuilder<TModel,string> builder)
where TModel : INamed
{
builder.Apply(new StringToUpperCase(), (model, changedName) =>
{
model = model with { Name = changedName };
return model;
});
return builder;
}
For reference
public interface INamed
{
string Name { get; }
}
There are two problems. One is that Name
does not have init
. I did not add it at the beginning because I would reuse this interface for classes and records. I decided that I will focus on records so I added init
. This did not solve anything. According to Rider TModel
is not valid record so I cannot use with
operator.
Is there a way to limit generic to work only for records?
As a workaround I could add two overloads for ForceUppercaseFormat()
format with Action<TModel, TProp>
and Func<TModel, TProp, TModel>
as mutators for class and record, but this does not look nice and a lot of extra code.
// for class
RuleFor(x => x.Name).ForceUppercaseFormat((x, newName) => x.Name = newName);
// for records
RuleFor(x => x.Name).ForceUppercaseFormat((x, newName) => x with {Name = newName});
Upvotes: 0
Views: 71
Reputation: 31198
A with
expression can be used with a record class, an anonymous type, or a value type.
There is no generic constraint to require a record class or an anonymous type. The only way you can use with
in a generic method is if the type is constrained to be a struct
. (And even then, ReSharper used to have a problem with that, so I can only assume Rider did as well.)
Probably the simplest option would be to define a mutation interface that all models would have to implement - eg:
public interface INamed
{
string Name { get; }
}
public interface INamed<TModel> : INamed where TModel : INamed<TModel>
{
TModel WithName(string nameName);
}
public record Foo(string Name) : INamed<Foo>
{
public Foo WithName(string nameName) => this with { Name = newName };
}
You could then add this constraint to your method:
public static ImmutableLogicRuleBuilder<TModel,string> ForceUppercaseFormat<TModel>(
this ImmutableLogicRuleBuilder<TModel,string> builder)
where TModel : INamed<TModel>
{
builder.Apply(new StringToUpperCase(), (model, changedName) =>
{
model = model.WithName(changedName);
return model;
});
return builder;
}
Upvotes: 1