Skip to content

Make validator lifetimes configurable on a validator-by-validator level#378

Merged
jchannon merged 1 commit intoCarterCommunity:net10from
enkelm:dynamic-validator-scope
Oct 9, 2025
Merged

Make validator lifetimes configurable on a validator-by-validator level#378
jchannon merged 1 commit intoCarterCommunity:net10from
enkelm:dynamic-validator-scope

Conversation

@enkelm
Copy link
Contributor

@enkelm enkelm commented Aug 16, 2025

While on a work related project, I came across a DI runtime exception for 'Scoped' lifetime Validators. After doing some digging, I came across this issue and then this PR which sets a global lifetime for all validators. This PR aims to add a method to set up custom logic based on type inference on a validator-by-validator level.

Implementation TLDR:

  • Introduces ValidatorServiceLifetimeFactory of type Func<Type, ServiceLifetime> on CarterConfigurator.
  • Default factory:
    • Checks for a [ValidatorLifetime] attribute on the validator type → uses its Lifetime.
    • If no attribute → falls back to ValidatorServiceLifetime (global default).
  • Users can override the factory via WithValidatorServiceLifetimeFactory(...) at startup.
  • CarterExtensions updated to register validators with lifetimes resolved per type.
  • IValidatorLocator lifetime is set to the lowest of all registered validator lifetimes.

Step by step:

On this first draft, it's implemented by the ValidatorServiceLifetimeFactory field on CarterConfigurator of type Func<Type, ServiceLifetime>. This func takes the validator type as an argument and must return a ServiceLifetime. It can be passed to the config on startup with the WithValidatorServiceLifetimeFactory but it also has a default implementation. By default, it checks for the newly added ValidatorLifetimeAttribute on the validator's type and return its Lifetime property or the config's ValidatorServiceLifetime if there isn't any attribute found.

This all comes together on CarterExtensions where the WireupCarter validator DI logic is changed to set the lifetime of each validator based on the result of ValidatorServiceLifetimeFactory. As for the IValidatorLocator lifetime, that's set to the lowest existing lifetime.

This is by no means ready to merge but more like a POC. I have tested it on my own app and seems to be working so far.

Note: Changing ValidatorServiceLifetime -> DefaultValidatorServiceLifetime is done for readability more than anything. I don't think it should go through since it breaksuser space.

@jchannon
Copy link
Member

jchannon commented Oct 8, 2025

@enkelm finally got around to this and think we should merge it for the .NET 10 release.

The only thing I don't really like is the attribute but not sure of another approach unless we use an interface. Thoughts?

@enkelm enkelm force-pushed the dynamic-validator-scope branch from a59e735 to 985d88b Compare October 8, 2025 14:04
@enkelm enkelm changed the base branch from main to net10 October 8, 2025 14:05
@enkelm
Copy link
Contributor Author

enkelm commented Oct 8, 2025

@jchannon changed the target branch to the .NET 10 upgrade.

We could for sure move to an interface approach, though I would argue it's a bit uglier since there isn't a way to pass enum entries as generics. We would have to make separate interfaces for each lifetime, like ISingletonValidator, IScopedValidator and so on.

Checking constructor DI to implicitly assign lifetimes would reduce the need to use the attribute for most users, so we could also go with that and keep interfaces/attributes for explicit lifetime assignment. This will be more prone to bugs and might be confusing to users though.


On a separate note, I noticed WithDefaultValidatorLifetime resets the factory func to always set said default lifetime.
Might have missed that on the initial commit, should that remain the case?

@jchannon
Copy link
Member

jchannon commented Oct 8, 2025

Ok stick with the attribute, it will be an edge case for users to do this IMO as validators for me should not need scoping or have business/IO logic in them but clearly people are doing that :)

@jchannon
Copy link
Member

jchannon commented Oct 8, 2025

Might be worth adding an example in the Carter.Samples app to show how you might use this new feature

@jchannon jchannon merged commit eb80a66 into CarterCommunity:net10 Oct 9, 2025
1 check failed
@jchannon
Copy link
Member

jchannon commented Oct 9, 2025

Thanks @enkelm ! Great work, thanks 👍

@jchannon jchannon added this to the vNext milestone Oct 9, 2025
@enkelm
Copy link
Contributor Author

enkelm commented Oct 10, 2025

Thanks for merging that in @jchannon 🙌! Let me know if you want me to add the example on the other branch.

jchannon pushed a commit that referenced this pull request Oct 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants