C
C#2w ago
KevS

✅ Passing subset of options when resolving instance with MS.DI (Working example with DryIoc)

@viceroypenguin Continuing the discussion we were having last night about moving away from DryIoc, I am currently using scope.Use(foo) when resolving instances at startup based on config and looking to simplify and just use MSDI. I created an example project to better illustrate how I'm currently wiring everything up https://github.com/ksladowski/device-registration-example
GitHub
GitHub - ksladowski/device-registration-example
Contribute to ksladowski/device-registration-example development by creating an account on GitHub.
43 Replies
viceroypenguin
nit: switch to .slnx file instead of .sln file asap. 🙂
KevS
KevSOP2w ago
Oh yeah my actual project is .slnx, I just threw this together quickly
viceroypenguin
you open to switching validation libraries?
KevS
KevSOP2w ago
Yeah for sure Are you talking about IV? I thought that was just for Handler parameters
viceroypenguin
nope. that's it's intended use, but techniclly there's only one class in IV that requires IH. the rest of IV works fully independently of IH there's an argument to be made that IV should have two packages, one for basic support and one for the glue class that binds IH and IV together your options class can look like this:
[Validate]
public sealed partial record DeviceOptions : IValidationTarget<DeviceOptions>
{
[NotEmpty]
public required string Id { get; init; }

[OneOf("A", "B")]
public required string Type { get; init; }

[Match(regex: nameof(IpAddressRegex))]
public required string IpAddress { get; init; }

[GeneratedRegex(@"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$")]
private static partial Regex IpAddressRegex { get; }
}
[Validate]
public sealed partial record DeviceOptions : IValidationTarget<DeviceOptions>
{
[NotEmpty]
public required string Id { get; init; }

[OneOf("A", "B")]
public required string Type { get; init; }

[Match(regex: nameof(IpAddressRegex))]
public required string IpAddress { get; init; }

[GeneratedRegex(@"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$")]
private static partial Regex IpAddressRegex { get; }
}
with no other validator class necessary. 🙂
KevS
KevSOP2w ago
So is it just the behavior part that binds IH and IV together?
viceroypenguin
correct
KevS
KevSOP2w ago
That's much nicer, I'll definitely switch those over And that all just works with services.AddOptions<T>().ValidateOnStartup() right?
viceroypenguin
public sealed class ValidationTargetOptions<TOptions> : IValidateOptions<TOptions>
where TOptions : class, IValidationTarget<TOptions>
{
public ValidateOptionsResult Validate(string? name, TOptions options)
{
ValidationResult result = TOptions.Validate(options);
if (result.IsValid)
return ValidateOptionsResult.Success;

IEnumerable<string> failures = result.Errors.Select(e =>
string.IsNullOrWhiteSpace(e.PropertyName)
? e.ErrorMessage
: $"{e.PropertyName}: {e.ErrorMessage}");

return ValidateOptionsResult.Fail(failures);
}
}
public sealed class ValidationTargetOptions<TOptions> : IValidateOptions<TOptions>
where TOptions : class, IValidationTarget<TOptions>
{
public ValidateOptionsResult Validate(string? name, TOptions options)
{
ValidationResult result = TOptions.Validate(options);
if (result.IsValid)
return ValidateOptionsResult.Success;

IEnumerable<string> failures = result.Errors.Select(e =>
string.IsNullOrWhiteSpace(e.PropertyName)
? e.ErrorMessage
: $"{e.PropertyName}: {e.ErrorMessage}");

return ValidateOptionsResult.Fail(failures);
}
}
KevS
KevSOP2w ago
Gotcha. Sweet well thats super nice. I was already using IV for endpoints anyway so this is a no brainer
viceroypenguin
sweet. now back to the original mission... haha ok, real question: why not just hand out IDeviceRegistry all over th place and let it return devices? or you could:
builder.Services.RegisterSingleton(sp =>
(string key) => sp.GetRequiredService<DeviceRegistry>().GetDevice(key)
);
builder.Services.RegisterSingleton(sp =>
(string key) => sp.GetRequiredService<DeviceRegistry>().GetDevice(key)
);
KevS
KevSOP2w ago
I'm doing this:
[Handler]
public sealed partial class DoWork(IDeviceRegistry deviceRegistry, ILogger<DoWork> logger)
{
public record Command(string DeviceId);

private async ValueTask HandleAsync(Command command, CancellationToken token)
{
try
{
IDevice device = deviceRegistry.GetDevice(command.DeviceId);
// business logic here
}
catch (NullReferenceException)
{
logger.LogError("Device with id {Id} not found", command.DeviceId);
return null;
}
}
}
[Handler]
public sealed partial class DoWork(IDeviceRegistry deviceRegistry, ILogger<DoWork> logger)
{
public record Command(string DeviceId);

private async ValueTask HandleAsync(Command command, CancellationToken token)
{
try
{
IDevice device = deviceRegistry.GetDevice(command.DeviceId);
// business logic here
}
catch (NullReferenceException)
{
logger.LogError("Device with id {Id} not found", command.DeviceId);
return null;
}
}
}
viceroypenguin
then you really don't need the dryioc container at all. just have DeviceRegistry receive your options and intiialize all of the devices in the constructor
KevS
KevSOP2w ago
I guess what I was trying to avoid was this: So if I need to add a new type of device I don't need to change code in multiple places
switch(options.Type)
{
case "A":
//create new A
case "B":
// etc etc
}
switch(options.Type)
{
case "A":
//create new A
case "B":
// etc etc
}
viceroypenguin
fair enough. do this...
builder.Services.AddSingleton<IDevice, DeviceA>();
builder.Services.AddSingleton<IDevice, DeviceB>();

public DeviceRegistry(
IEnumerable<IDevice> devices,
IOptions<DeviceOptionsCollection> options
)
{
foreach (var d in options.Value.Options)
{
}
}
builder.Services.AddSingleton<IDevice, DeviceA>();
builder.Services.AddSingleton<IDevice, DeviceB>();

public DeviceRegistry(
IEnumerable<IDevice> devices,
IOptions<DeviceOptionsCollection> options
)
{
foreach (var d in options.Value.Options)
{
}
}
KevS
KevSOP2w ago
I typically register them with Injectio attributes, and before that I did something worse:
List<string> keys = ReflectionHelpers.GetImplementationsFromNamespace(
baseNs,
typeof(IDevice)
);

foreach (string key in keys)
{
Type? deviceType = typeof(DeviceStartupExtensions).Assembly.GetType(
$"{baseNs}.{key}.{key}Device",
throwOnError: false
);
if (deviceType != null)
container.Register(
serviceType: typeof(IDevice),
implementationType: deviceType,
reuse: Reuse.Scoped,
serviceKey: key
);
}
List<string> keys = ReflectionHelpers.GetImplementationsFromNamespace(
baseNs,
typeof(IDevice)
);

foreach (string key in keys)
{
Type? deviceType = typeof(DeviceStartupExtensions).Assembly.GetType(
$"{baseNs}.{key}.{key}Device",
throwOnError: false
);
if (deviceType != null)
container.Register(
serviceType: typeof(IDevice),
implementationType: deviceType,
reuse: Reuse.Scoped,
serviceKey: key
);
}
And scoped isn't really correct here, they probably should be transient DryIoc doesn't like transients that are disposable though
viceroypenguin
transient and singleton are effectively the same when the thing asking for it is singleton, so better to just register them singleton
KevS
KevSOP2w ago
But I might need multiple DeviceAs that are distinct
viceroypenguin
ahhh... good to know ah, got it. hold on a sec
KevS
KevSOP2w ago
Yeah I put the options in appsettings.json
viceroypenguin
honestly, this switch is probably your best bet. keyed services won't work the way i want them to either.
var device = options.Type switch
{
"A" => new DeviceA(options),
"B" => new DeviceB(options),
...
};
var device = options.Type switch
{
"A" => new DeviceA(options),
"B" => new DeviceB(options),
...
};
you could even isolate to a method that you source generate, if you wanted
private IDevice GetDevice(DeviceOptions options) =>
options.Type switch ......
private IDevice GetDevice(DeviceOptions options) =>
options.Type switch ......
be a really easy generator too. you could do it by reflection too if you wanted if that was easier reflection just isn't on my shelf of "i'm going to use that" anymore. 🙂
KevS
KevSOP2w ago
I might go the source gen route then. I have another really basic one for json polymorphic types. I wouldn't be opposed to just keeping dryioc either since it appears I actually have one of the niche cases where I need the functionality it adds over MS.DI, but idk how I feel about dropping a preview version into production code, especially since I can't seem to find a changelog between 5.4.3 and 6.0.0-preview
viceroypenguin
yeah, i just haven't found that i use the dryioc features anymore
KevS
KevSOP2w ago
Also, important but unrelated question now that I think about it - IH Behaviors are meant for cross-cutting concerns. I have a couple decorators too that are business logic but I want to keep the code separate from the remaining business logic that it wraps. Are Behaviors appropriate here or would i need to keep Decorator<> too
viceroypenguin
that's totally appropriate behaviors can be what you want them to be. anything that's cross-cutting. whether that's infra (logging), biz concerns, or something that spreads both (like authz) like https://github.com/viceroypenguin/VsaTemplate/blob/master/Api/Features/Todos/Authorization/TodoAuthorizationBehavior.cs is technically crossing both biz logic and infra.
KevS
KevSOP2w ago
Got it. So do you not use TransformResult? You touched on this the other day and I'm still a bit confused. Sorry to keep throwing more questions at you
viceroypenguin
Sorry to keep throwing more questions at you
all good. anythign to spread the mission of IP... 😄
So do you not use TransformResult?
nope. i'll argue the best way to use it for those that do want to use it, but i have no desire for it personally
KevS
KevSOP2w ago
Other than adding the openapi annotations, I guess I just don't get why separate the business logic from the endpoint logic if the business logic isn't able (or recommended) to be reused elsewhere anyway
viceroypenguin
the reason one would use the transformresult is if they wanted the endpoint code to be purely business logic, unaware of web at all. so you return a regular command object from your handleasync, and let the transform turn it into something useful my opinion is that there are endpoints that are just plain awkward to separate biz code from the fact that it's a web endpoint (for example, if you want to return a streamresult of some kind) besides, i don't bother with typedresults. i just return the object i need, and throw an exception for various semi-expected cases. the exceptions are usually custom exceptions that then expose http status code and message, and the eproblem details handler picks those values up and uses them
KevS
KevSOP2w ago
Okay so thats the piece I don't really get how it's wired up. It's endpoint filters or something like that right?
KevS
KevSOP2w ago
Okay, and problem details is just an ASP.NET thing I wasn't aware of?
viceroypenguin
yes technically a web thing, that aspnet now has support for, but yes.
KevS
KevSOP2w ago
Gotcha. Just about things I see in your template now - does [LoggerMessage] provide anything other than organization? Like it still uses serilog via the Microsoft.Logging.Extensions adapter right
KevS
KevSOP2w ago
Oh interesting, I wasn't ever aware of that either Okay, well I can't think of anything else, I really appreciate you taking the time to look at this and answer my questions
viceroypenguin
you bet!
KevS
KevSOP2w ago
Question - I have a lot of stuff like this:
RuleFor(x => x.InputDirectory)
.NotEmpty()
.Must(Directory.Exists)
.When(x => !IpAddressIsRequired(x.Type))
RuleFor(x => x.InputDirectory)
.NotEmpty()
.Must(Directory.Exists)
.When(x => !IpAddressIsRequired(x.Type))
A lot of my validations have conditionals and unless I'm missing something, I think doing AdditionalValidations would be a significant increase in boilerplate compared to IV, no? 🙁
viceroypenguin
do me a favor, open a new issue? i do havee an answer for you though 🙂 @KevS fyi i meant in #help. not on IV. hope you're not creating a GH issue... haha
KevS
KevSOP2w ago
Yep I got it, just posted it haha. Just had to take the dog outside quick Honestly I just ended up with this:
var device = _serviceProvider.GetRequiredKeyedService<IDevice>(deviceOptions.Type);
device.Configure(deviceOptions);
var device = _serviceProvider.GetRequiredKeyedService<IDevice>(deviceOptions.Type);
device.Configure(deviceOptions);
It's not ideal cause the properties initialize like this
public string IpAddress { get; private set; } = "";
public string IpAddress { get; private set; } = "";
But since I'm validating the options I think it's okay
viceroypenguin
why this instead of generating the switch?
KevS
KevSOP2w ago
I was just trying it out. I have 3 different constructs that behave this way and seemed like it might be simpler but after sleeping on it, not so sure anymore. It feels like its a lot less code and I don't really see a downside Okay i actually needed a validator too so I tried again, if I open an issue in code-review later would you mind taking a look?
viceroypenguin
of course

Did you find this page helpful?