Marek Wdowiak
Marek Wdowiak

Reputation: 23

C# records, is there a way to property Name is a part of a record so I can use with expression?

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

Answers (1)

Richard Deeming
Richard Deeming

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

Related Questions