How to avoid repeating conditional logic? - c#

I'm implementing security features for my .NET Core application and I'm finding myself repeating the same conditional logic over and over.
Is there a way where I can generalize this in one place and have it applied to the segments I want?
I recall using delegates or Func for this type of thing but I'm not quite sure... Any ideas?
Below is the code I'm trying to write once and apply in multiple places.
var currentUser = _httpContext.HttpContext.Session.GetCurrentUser<SessionContext>();
if(currentUser.Roles.Any())
{
// ex query here. This could be any piece of code
var q = from u in _dbContext.Users
join d in _dbContext.Users on u.DoctorId equals d.Id into ud
from docU in ud.DefaultIfEmpty()
select new
{
User = u,
Doctor = docU
};
if(!currentUser.Roles.Contains("Administrator"))
{
if(currentUser.Roles.Contains("Doctor"))
{
//do something here
//ex.
q = q.Where(x => (x.Doctor != null ? x.Doctor.Id == currentUserId : false));
}
else if (currentUser.Roles.Contains("Patient"))
{
//do something else here
//ex.
q = q.Where(x => x.User.Id == currentUserId);
}
}
}
else
throw new Exception("No roles applied to logged in user");

Here's some code written in Swift.
I'm using functional oriented programming, with dictionaries
struct User {
var Roles: Set<String> = ["Doctor"]
}
func channel(user: User, _ roles: [String:() -> ()]) {
for i in roles {
if user.Roles.contains(i.key) { i.value() }
}
}
let currentUser = User()
channel(user: currentUser,
[
"Doctor": {
// Code for doctor
},
"Admin": {
// Code for admin
},
"Blah": {
// Code for blah
},
// You can even add more
]
)
You can enum Create an Enum
Why an Enum?
You can easily make typos with regular Strings
With an Enum, if you make a typo, Swift gives you an error. Super Helpful!
enum UserRolls { case doctor, admin, patient, other(String) }
extension UserRolls: Hashable {}
struct User {
var Roles: Set<UserRolls> = [.doctor]
}
func channel(user: User, _ roles: [UserRolls:() -> ()]) {
for i in roles {
if user.Roles.contains(i.key) { i.value() }
}
}
let currentUser = User()
channel(user: currentUser,
[
.doctor: {
// Code for doctor
},
.admin: {
// Code for admin
},
.other("Blah"): {
// Code for blah
},
// You can even add more
]
)

You could create a new service.
public class MyHttpContextService : IMyHttpContextService
{
IHttpContextAccessor _httpContext;
public MyHttpContextService(IHttpContextAccessor httpContext)
{
_httpContext = httpContext;
}
public string CheckUserRoles()
{
try
{
var currentUser = _httpContext?.HttpContext?.Session?.GetCurrentUser<SessionContext>();
if (currentUser != null)
{
if(currentUser.Roles.Any())
{
if(!currentUser.Roles.Contains("Administrator"))
{
if(currentUser.Roles.Contains("Doctor"))
{
//do something here
}
else if (currentUser.Roles.Contains("Patient"))
{
//do something else here
}
}
}
}
else
{
// if currentUser == null
}
}
catch (Exception ex)
{
// exception handling
}
}
}
Notice the line
var currentUser = _httpContext.HttpContext.Session.GetCurrentUser<SessionContext>();
is replaced by
var currentUser = _httpContext?.HttpContext?.Session?.GetCurrentUser<SessionContext>();
Create appropriate interface.
public interface IMyHttpContextService
{
string CheckUserRoles();
}
In this example string is return type, but it does not need to be.
Finally, register this service using line
services.AddScoped<IMyHttpContextService, MyHttpContextService>();
where services is
IServiceCollection services
Instead of AddScoped, you could use AddTransient, or AddSingleton. More about objects' lifetime and dependency injection. These three keywords determine the lifetime of an object, or in this case, of service.
Registration is starting in Startup.cs (but to be honest, everything is starting in Startup.cs, hence the name). More about Startup.cs. In other words, in Startup.cs call method
public void ConfigureServices(IServiceCollection services)
which is called by runtime.
Inside method ConfigureServices call another one, for instance MapInterfaces, for interface mapping and pass it services. Method MapInterfaces will be a static method inside ServiceExtensions.cs.
public static void MapInterfaces(IServiceCollection services)
{
services.AddScoped<IMyHttpContextService, MyHttpContextService>();
}
Even better is to create an extension method More about extension methods.
ServiceExtensions.cs is a static class, which is a condition for creating extension method. Extension method also needs to be static. This would be the methods signature
static void MapInterfaces(this IServiceCollection services)
Of course, don't forget the access modifier (at least the same visibility as the ServiceExtensions.cs class). Notice this keyword.
Extension method MapInterfaces from ServiceExtensions.cs is then called inside ConfigureServices method from Startup.cs like this
services.MapInterfaces();
In the end, whenever you need method CheckUserRoles, call it like this
_myHttpContextService.CheckUserRoles();
EDIT: You've changed the implementation of method, but that doesn't change the way you could do the rest of solution.

Related

Create an IEnumerable<AbstractClass> of specific instances from different classes all inheriting from the abstract class

I have controllers which, for the sake of backwards compatibility, only have one action. The JSON request comes with an attribute "type" which determines what the action should do with it.
My idea for a clean solution was to build a set of action handlers. They all inherit from an abstract class called ActionHandler which has two methods
public abstract bool CanHandle(ClientRequest request);
and
public abstract object Handle(dynamic request)
And it has a property
public abstract string ActionForController { get; }
in which the specific actionhandlers just return the name of the controller they want to handle for. This is not very important, but may help clarify something later.
The controller is inserted with an ActionHandlerRegister which has an IEnumerable and a method "GetActionHandler". It returns the first specific ActionHandler that can handle the request.
public ActionHandler GetActionHandler(ClientRequest request)
{
foreach(var actionHandler in ActionHandlers)
{
if (actionHandler.CanHandle(request))
{
return actionHandler;
}
}
throw new BadRequestException(string.Format(CultureInfo.InvariantCulture, BadRequestExceptionTemplate, request.Type));
}
The controllers look like this:
public class LogController : ControllerBase
{
private readonly IActionHandlerRegister<LogController> logHandlers;
public LogController(IActionHandlerRegister<LogController> logHandlers)
{
this.logHandlers = logHandlers ?? throw new ArgumentNullException(nameof(logHandlers));
}
[HttpPost]
public async Task<IActionResult> Post([FromBody] dynamic rawJson)
{
var jsonBody = ((JsonElement)rawJson).ToString();
if (string.IsNullOrEmpty(jsonBody))
{
return BadRequest(ActionHandler.BadRequestRequestNullOrTypeMissingError);
}
var clientRequest = JsonSerializer.Deserialize<ClientRequest>(jsonBody);
if (clientRequest == null || string.IsNullOrEmpty(clientRequest.Type))
{
return BadRequest(ActionHandler.BadRequestRequestNullOrTypeMissingError);
}
try
{
var handler = logHandlers.GetActionHandler(clientRequest);
var result = handler.Handle(rawJson);
return Ok(result);
}
catch (BadRequestException ex)
{
return BadRequest(ex.Message);
}
}
}
For people paying attention: yes, I'm passing the rawjson to handler.Handle. This is because "ClientRequest" is something generic (from which I can read the type) but the handler needs the specific request, so it's deserializing again to something more specific. Maybe there are better solutions for that. Feel free to tell me.
In startup.cs, the insertion of the ActionHandlerRegister into the controller looks like this:
public void RegisterActionHandlersAsSingleton(IServiceCollection services)
{
IEnumerable<ActionHandler> listOfActionHandlers =
from domainAssembly in AppDomain.CurrentDomain.GetAssemblies()
from actionHandlerType in domainAssembly.GetTypes()
where actionHandlerType.IsAssignableFrom(typeof(ActionHandler))
select (ActionHandler)Activator.CreateInstance(actionHandlerType);
services.AddSingleton<IActionHandlerRegister<LogController>>(new ActionHandlerRegister<LogController>(listOfActionHandlers.Where(a => a.ActionForController == nameof(LogController))));
// other controllers ...
}
You might be able to guess, this last piece of code crashes at runtime telling me it's unable to cast to ActionHandler.
System.InvalidCastException: Unable to cast object of type
'System.Object' to type
'TcServerModules.ActionHandlers.ActionHandler'.
I have been playing around with different solutions, but none of them scratch that itch. What would be a nice, true-to OO-design principle

Scoped lifetime services become singletons when injected through controllers

I have a controller in a .NET Core application:
public FriendsController(IFriendRepository friendRepository)
{
this.friendRepository= friendRepository;
}
The IFriendRepository is an interface which is implemented with the class:
public class FriendRepository : IFriendRepository {
...
}
In Startup I set it up by using the following line in ConfigureServices() :
services.AddScoped<IFriendRepository , FriendRepository >();
However, when the controller is used, FriendRepository is has the lifetime set as a singleton instead of scoped. The reason I was able to find was on this page:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1
Under Service lifetimes, Scoped. It shows:
I do not understand how to use Invoke instead of a constructor. The example they use is for a custom middleware, which I at least can't wrap my head on how to interpret it for a constructor.
public class FriendRepository : IFriendRepository
{
private readonly ManagementDbContext dbContext;
public FriendRepository(ManagementDbContext dbContext)
{
this.dbContext = dbContext;
}
public void Add(Friend friend)
{
this.dbContext.Friends.Add(friend);
}
public void Remove(Friend friend)
{
this.dbContext.Remove(friend);
}
public void Update(Friend friend)
{
this.dbContext.Update(friend);
}
}
The following is "GetFriends", inside FriendRepository:
public async Task<QueryResult<Friend>> GetFriendsAsync(FriendQuery queryObj)
{
var result = new QueryResult<Friend>();
var query = dbContext.Friends
.Include(c => c.Type)
.AsQueryable();
if(queryObj.TypeId.HasValue)
{
query = query.Where(c => c.Type.Id == queryObj.TypeId);
}
if(queryObj.Name != null && queryObj.Name.Length > 0)
{
query = query.Where(c => c.Name.Contains(queryObj.Name));
}
// todo add total price here
var columnsMap = new Dictionary<string, Expression<Func<Calculation, object>>>()
{
["id"] = c => c.Id,
["name"] = c => c.Name,
["type"] = c => c.Type,
["totalFriends"] = c => c.TotalFriends,
["createdTime"] = c => c.CreatedTime
};
query = query.ApplyOrdering(queryObj, columnsMap);
result.TotalItems = await query.CountAsync();
query = query.ApplyPaging(queryObj);
result.Items = await query.ToListAsync();
return result;
}
I solved it, I will first explain my assumption, since the fix might be very much limited to my scenario.
I have all of my DBContext used in 3 repositories. They all use async functions however they all contain awaits inside for any of the async functions used inside of them.
The issue seemed to only occur once I started using these repositories as before I was accessing the dbContext directly in the Controller. This made me consider the problems in the link, which I also posted a picture of in the question:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1
Even though it specified middle ware only, I assumed it was worth a chance since I couldn't figure any other problem.
Now as for the actual problem. One of my functions in the UserRepository, GetUser() is an async method, and even though the error seemed to be in the FriendRepository methods, since they were always the ones crashing, it turns out that the GetUser() function was used once in startup under AddJwtBearer without await.
I had assumed that since it had an await inside of it, it would not create a problem. I also had not noticed this was a problem since I was so focused on the other repository. My hope was that maybe I was missing something as simple as the dependency injection through a constructor in middleware switching lifetime regardless of what the lifetime was already set to.
For anyone else in the future, I ended up doing 2 things which allowed me to clearly debug my application step by step.
I created a Logger static class which allows me to save text to file easily. I use this to log functions being used, constructors etc. This let me ensure that I could track the amount of times constructors and functions were called, in what order and which ones would not be reached. Here is the Logger for anyone else:
public static class Logger
{
public static void Log(string text, string fileName)
{
string path = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + "/" + fileName;
bool done = false;
while (!done)
{
done = true;
try
{
FileStream fileStream = null;
fileStream = System.IO.File.Open(path, System.IO.File.Exists(path) ? FileMode.Append : FileMode.OpenOrCreate);
using (StreamWriter fs = new StreamWriter(fileStream))
{
fs.WriteLine(text);
};
fileStream.Close();
}
catch (IOException)
{
done = false;
}
}
}
public static void Log(string text)
{
Log(text, "logger.txt");
}
}
I added a string to the DBContext and whenever I use it in any function I would add the name of the function after the name of the class it is used in. So if my FriendsRepository would use it in a function GetTypes, it would look like:
myDbContext.methodUsing = "FriendsRepository>GetTypes()";
Thank you to #GuruStron for being patient and giving me advice on how to take this step by step, explaining to me that the middleware error idea had no leg to stand on and suggesting to me on how to approach debugging.

why password is validated twice?

I'm configuring ASP.NET Core Identity's password validations with custom validations, so in the startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentity<AppUser, IdentityRole>( opts => {
opts.Password.RequiredLength = 6;
}).AddEntityFrameworkStores<AppIdentityDbContext>().AddDefaultTokenProviders();
services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
...
}
and my customer password validator is
public class CustomPasswordValidator : PasswordValidator<AppUser>
{
public override async Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user, string password)
{
IdentityResult result = await base.ValidateAsync(manager, user, password);
List<IdentityError> errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList();
if (password.ToLower().Contains(user.UserName.ToLower()))
{
errors.Add(new IdentityError
{
Code = "PasswordContainsUserName",
Description = "Password cannot contain username"
});
}
return errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
}
}
and when I ran the app and typed an invalid password whose length < 6, there is a duplicated validation output as:
Passwords must be at least 6 characters.
Passwords must be at least 6 characters.
I guess it is because I called the base's ValidateAsync()(which contains the validation login in the startup.cs), but isn't that my CustomPasswordValidator override base's ValidateAsync(), so the base's validation should only be called once?
services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
This call doesn't replace the IPasswordValidator<AppUser> registration already added by the call to AddIdentity; it adds another. This means you end up with two password-validators, both of which check the same set of built-in rules.
Usually, when requesting a type from DI, we ask for a single implementation. Here's an example constructor:
public SomeClass(ISomeService someService) { }
If two implementations have been registered for ISomeService, this constructor is given an instance of the one that is registered last. However, we can still get both instances by updating the constructor to request a collection of ISomeService. Here's an example of that:
public SomeClass(IEnumerable<ISomeService> someServices) { }
In this scenario, with two registered implementations of ISomeService, someServices contains instances of both implementations. This is exactly what happens in UserManager, which was designed to support multiple validators.
Viewing the source for UserManager.ValidatePasswordAsync shows how the validators are enumerated and executed in sequence:
foreach (var v in PasswordValidators)
{
var result = await v.ValidateAsync(this, user, password);
if (!result.Succeeded)
{
errors.AddRange(result.Errors);
}
}
This means that, instead of extending PasswordValidator<AppUser>, CustomPasswordValidator can just implement IPasswordValidator<AppUser> directly:
public class CustomPasswordValidator : IPasswordValidator<AppUser>
{
public async Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user, string password)
{
// ...
}
}
The code inside your implementation method stays the same, except for it calling into base.
I find that you need to register your CustomPasswordValidator before the service.AddIdentity
services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
services.AddIdentity<AppUser, IdentityRole>( opts => {
opts.Password.RequiredLength = 6;
}).AddEntityFrameworkStores<AppIdentityDbContext>().AddDefaultTokenProviders();

How to reuse data in FluentValidation

For example I have validator with two validation rules:
// Rule 1
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) != 0)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with provided Email in database!");
As you can see there are two calls to database with same method. How do I call it once and reuse the data for other rules?
Another issue when displaying error messages:
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!",
(model, email) => { return email; });
Is there a better way to display error messages not all the time writing those lambda expressions to retrieve property? Like saving model somewhere and then use it later. Simple and easy to implement solutions would be nice!
For #1, There isn't a way to do this I'm afraid. Validators are designed to be stateless so they can be reused across threads (in fact, it's highly recommended you create validator instances as singletons as they're very expensive to instantiate. The MVC integration does this by default). Don't mess with static fields as you'll run into threading issues.
(Edit: in this particular simple case you can just combine the rules into a single call to Must, but in general you can't share state between rules)
For #2, This depends on the property validator you're using. Most property validators actually allow you to use the {PropertyValue} placeholder, and the value will automatically be inserted. However, in this case you're using the "Must" validator (PredicateValidator) which doesn't support placeholders.
I have a list of which validators support custom placeholders here: https://github.com/JeremySkinner/FluentValidation/wiki/c.-Built-In-Validators
Just came across this question while looking for a better way ;)
Another way is to override the ValidateAsync and Validate methods and store the result in a local field which can be accessed by the rules as follows:
public class MyValidator : AbstractValidator<MyCommand>
{
User _user = User.Empty;
public MyValidator()
{
RuleFor(o => o.Email)
.Must((_) => !_user.IsEmpty)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
//other rules which can check _user
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<MyCommand> context, CancellationToken cancellation = default)
{
var cmd = context.InstanceToValidate;
// you could wrap in a try block if this throws, here I'm assuming empty user
_user = await _repository.GetUser(cmd.Email);
return await base.ValidateAsync(context, cancellation);
}
public override ValidationResult Validate(ValidationContext<SubmitDecisionCommand> context) => ValidateAsync(context).Result;
}
Part 1
You want to reduce database calls from 2 to 1, so you need to use field to save database call result, because validator rules code actually work in "runtime".
Validator class:
public class MyValidator : Validator<UserAccount>
{
private int? _countOfExistingMails;
private string _currentEmail;
private object locker = new object();
public MyValidator()
{
CallEmailValidations();
// other rules...
}
}
Here is separate method for mail validation calls. As far as Must take expression as parameter, you can pass method name with it's arguments:
public void CallEmailValidations()
{
RuleFor(o => o.Email).Must(x => EmailValidation(x, 0))
.WithMessage("User with provided Email was not found in database!");
RuleFor(o => o.Email).Must(x => EmailValidation(x, 1))
.WithMessage("There are multiple users with provided Email in database!");
}
And validation method's body itself:
public bool EmailValidation(string email, int requiredCount)
{
var isValid = false;
lock(locker)
{
if (email != _currentEmail || _currentEmail == null)
{
_currentEmail = email;
_countOfExistingMails = (int)GetDataDataFromDB(email);
}
if (requiredCount == 0)
{
isValid = _countOfExistingMails != 0; // Rule 1
}
else if (requiredCount == 1)
{
isValid = _countOfExistingMails <= 1; // Rule 2
}
}
// Rule N...
return isValid;
}
UPDATE:
This code works, but better approach is to implement caching in data access layer method.
Part 2
Here is rewritten rule:
RuleFor(o => o.Email).Must((email) => GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!", m => m.Email)
From "C# in depth":
When the lambda expression only needs a single parameter, and that
parameter can be implicitly typed, C# 3 allows you to omit the
parentheses, so it now has this form
GOTCHAS:
Do not pass explicitly this to lambda-expressions. It could cause preformance issues as I know. There is no reason to create extra-closure.
I suppose you use DataContext in some form inside GetDataDataFromDB method. So you have to control lifetime of your context, because validator object instantiated as singletone.
What you can do is to use WhenAsync. I have created an extension method to make things easier.
public static class ValidatorExtensions
{
public static void ResolveDataAsync<TEntity, TData>(
this AbstractValidator<TEntity> validator,
Func<TEntity, CancellationToken, Task<TData>> resolver,
Action<ValueAccessor<TData>> continuation)
{
TData data = default;
var isInitialized = false;
var valueAccessor = new ValueAccessor<TData>(() =>
{
if (!isInitialized)
{
throw new InvalidOperationException("Value is not initialized at this point.");
}
return data;
});
validator.WhenAsync(async (entity, token) =>
{
data = await resolver(entity, token);
return isInitialized = true;
},
() => continuation(valueAccessor));
}
}
public class ValueAccessor<T>
{
private readonly Func<T> _accessor;
public ValueAccessor([NotNull] Func<T> accessor)
{
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
}
public T Value => _accessor();
}
Usage:
public class ItemCreateCommandValidator : AbstractValidator<ItemCreateCommand>
{
private readonly ICategoryRepository _categoryRepository;
public ItemCreateCommandValidator(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
this.ResolveDataAsync(CategoryResolver, data =>
{
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(x => data.Value.ContainsKey(x))
.WithMessage((_, id) => $"Category with id {id} not found."));
});
}
private Func<ItemCreateCommand, CancellationToken, Task<Dictionary<int, Category>>> CategoryResolver =>
async (command, token) =>
{
var categories = await _categoryRepository.GetByIdsAsync(command.SubcategoryIds, token);
return categories.ToDictionary(x => x.Id);
};
}
Works fine to me, but there are a few GOTCHAS:
The validator usually have to be defined as Scoped or Transient (Scoped is better for performance) in order to be compatible with lifecycle of it's dependencies (e.g. repository passed in constructor).
You can't access the data.Value right inside ResolveDataAsync callback. This is because the value is not initialized by that time. By this time validator is in creation phase and ValidateAsync method was not called => nothing to validate => value can't be accessed.
It can be used only in AbstractValidator methods:
this.ResolveDataAsync(CategoryResolver, data =>
{
var value = data.Value; // Throws InvalidOperationException
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(data.Value.ContainsKey) // Also throws
.WithMessage((_, id) => $"Category with id {id} not found."));
});
These gotchas also occur with other approaches, such as overriding the ValidateAsync method, and there is not much you can do about them.
You can also call ResolveDataAsync with different resolvers depending on condition when using WhenAsync, UnlessAsync. This will help you not to load data that is not needed in all cases every time:
WhenAsync(myCondition1, () => this.ResolveDataAsync(myResolver1, data => { ... }))
UnlessAsync(myCondition2, () => this.ResolveDataAsync(myResolver2, data => { ... }))

Using Autofac's IIndex to resolve multiple Keyed instances

I'd like to use AutoFac in a way that both a State and Strategy pattern coexist. After researching how, I got familiar with the Keyed/Named registration of Autofac and used this for my states using the passive IIndex method. After this, I was looking at the Strategy pattern, which to me looked like a good way of using the same idea, with resolving IIndex for both State and Strategy. I've saved my Strategy options in the same way (enum) as State and Keyed them in the DependencyResolver:
builder.RegisterType<NewAanvraag>().Keyed<IAanvraagState>(AanvraagState.Nieuw).Keyed<IAanvraagState>(BusinessState.Default);
builder.RegisterType<RareNewAanvraag>().Keyed<IAanvraagState>(AanvraagState.Nieuw).Keyed<IAanvraagState>(BusinessState.Rare);
builder.RegisterType<OpvoerenInformatie>().Keyed<IAanvraagState>(AanvraagState.OpvoerenInformatie).Keyed<IAanvraagState>(BusinessState.Default);
This way, I would like to use both options to be created in dynamic order, whereas some implementations might be the same as the default, and some are not.
However, when trying to access both the state and the strategy, I got a notion of KeyedServiceIndex2 (DelegateActivator), but neither option could be resolved by itself
private readonly IIndex<AanvraagState, IAanvraagState> _states;
private readonly IIndex<BusinessState, IAanvraagState> _strategyState;
public IAanvraagDto AanvraagDto { get; set; }
private IAanvraagState CurrentState{ get { return _states[AanvraagDto.State];} }
private IAanvraagState CurrentStrategy { get { return _strategyState[AanvraagDto.BusinessState]; } }
public Aanvraag(IIndex<AanvraagState, IAanvraagState> states, IIndex<BusinessState, IAanvraagState> strategyState)
{
_states = states;
_strategyState = strategyState;
}
public void Start()
{
CurrentStrategy.Start(AanvraagDto);
SetState(AanvraagState.OpvoerenInformatie);
}
When I tried to use both it couldn't find the implementation (also tried IIndex<BusinessState, IIndex<AanvraagState, IAanvraagState>>):
private readonly IIndex<AanvraagState, IIndex<BusinessState, IAanvraagState>> _states;
public IAanvraagDto AanvraagDto { get; set; }
private IAanvraagState CurrentState { get { return _states[AanvraagDto.State][AanvraagDto.BusinessState]; } }
public Aanvraag(IIndex<AanvraagState, IIndex<BusinessState, IAanvraagState>> states)
{
_states = states;
}
public void Start()
{
CurrentState.Start(AanvraagDto);
SetState(AanvraagState.OpvoerenInformatie);
}
Does anyone know how to use 2 Keyed variables to retrieve a grid-like structure for resolving the concrete implementation?
PS: This is the first question I ask on StackOverflow, so any constructive feedback is highly appreciated.
The IIndex<K,V> relationship is really just for single-dimension keyed services. It won't work for multi-dimensional selection.
What you're more likely looking for is component metadata, the ability to associate any arbitrary data with a registration and select the registration based on that data.
The documentation has some great examples and details, but I'll show you a simple example that might fit closely with what you're doing.
First, you need to define a metadata class. This is the thing that will track the various "dimensions" of the "matrix" by which you want to select your component. I'll do something simple here - two Boolean fields so there are only four total combinations of metadata available:
public class ServiceMetadata
{
public bool ApplicationState { get; set; }
public bool BusinessState { get; set; }
}
I'll use some very simple empty services just for illustration. Yours will obviously do something more. Note I have four services - one for each combination of metadata.
// Simple interface defining the "service."
public interface IService { }
// Four different services - one for each
// combination of application and business state
// (e.g., ApplicationState=true, BusinessState=false).
public class FirstService : IService { }
public class SecondService : IService { }
public class ThirdService : IService { }
public class FourthService : IService { }
Here's where you consume the services. To more easily take advantage of the strongly-typed metadata, you'll need to reference System.ComponentModel.Composition so you have access to System.Lazy<T, TMetadata>.
public class Consumer
{
private IEnumerable<Lazy<IService, ServiceMetadata>> _services;
public Consumer(IEnumerable<Lazy<IService, ServiceMetadata>> services)
{
this._services = services;
}
public void DoWork(bool applicationState, bool businessState)
{
// Select the service using LINQ against the metadata.
var service =
this._services
.First(s =>
s.Metadata.ApplicationState == applicationState &&
s.Metadata.BusinessState == businessState)
.Value;
// Do whatever work you need with the selected service.
Console.WriteLine("Service = {0}", service.GetType());
}
}
When you do your registrations, you'll need to register the metadata along with the components so they know which combination of data they belong to.
var builder = new ContainerBuilder();
builder.RegisterType<Consumer>();
builder.RegisterType<FirstService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, false);
m.For(sm => sm.BusinessState, false);
});
builder.RegisterType<SecondService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, false);
m.For(sm => sm.BusinessState, true);
});
builder.RegisterType<ThirdService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, true);
m.For(sm => sm.BusinessState, false);
});
builder.RegisterType<FourthService>()
.As<IService>()
.WithMetadata<ServiceMetadata>(m => {
m.For(sm => sm.ApplicationState, true);
m.For(sm => sm.BusinessState, true);
});
var container = builder.Build();
Finally, you can then use your consumer class to get services by "matrix," as you say. This code:
using(var scope = container.BeginLifetimeScope())
{
var consumer = scope.Resolve<Consumer>();
consumer.DoWork(false, false);
consumer.DoWork(false, true);
consumer.DoWork(true, false);
consumer.DoWork(true, true);
}
Will yield this on the console:
Service = FirstService
Service = SecondService
Service = ThirdService
Service = FourthService
Again, you'll definitely want to check out the documentation for additional details and examples. It will add clarification and help you understand other options you have available to maybe make this easier or work better in your system.

Categories

Resources