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 => { ... }))
Related
BusinessAction is used to represent an action that can be performed by a user. Each action is related to the specific entity, so if for example, that entity is Order, business actions could be CancelOrder, IssueRefund, etc.
public abstract class BusinessAction<T>
{
public Guid Id { get; init; }
public Func<T, bool> IsEnabledFor { get; init; }
}
public class CancelOrderAction : BusinessAction<Order>
{
public CancelOrderAction ()
{
Id = Guid.Parse("0e07d05c-6298-4c56-87d7-d2ca339fee1e");
IsEnabledFor = o => o.Status == OrderStatus.Active;
}
}
Then I need to group all actions related to the specific type.
public interface IActionRegistry
{
Task<IEnumerable<Guid>> GetEnabledActionIdsForAsync(Guid entityId);
}
public class ActionRegistry<T> : IActionRegistry
where T : BaseEntity
{
private readonly IEnumerable<BusinessAction<T>> _actions;
private readonly IRepository<T> _repository;
public ActionRegistry(IEnumerable<BusinessAction<T>> actions, IRepository<T> repository)
{
_actions = actions;
_repository = repository;
}
public async Task<IEnumerable<Guid>> GetEnabledActionIdsForAsync(Guid entityId)
{
var entity = await _repository.FindByIdAsync(entityId);
return entity == null
? Enumerable.Empty<Guid>()
: _actions.Where(a => a.IsEnabledFor(entity)).Select(a => a.Id);
}
}
Finally, there is an API endpoint that receives entity type (some enumeration that is later on mapped to real .NET type) and ID of an entity. The API endpoint is responsible to return action IDs that are enabled for the current state of the entity.
public class RequestHandler : IRequestHandler<Request, IEnumerable<Guid>>>
{
private readonly Func<Type, IActionRegistry> _registryFactory;
public RequestHandler(Func<Type, IActionRegistry> registryFactory)
{
_registryFactory = registryFactory;
}
public async Task<IEnumerable<Guid>> Handle(Request request, CancellationToken cancellationToken)
{
var type = request.EntityType.GetDotnetType();
var actionRegistry = _registryFactory(type);
var enabledActions = await actionRegistry.GetEnabledActionIdsForAsync(request.EntityId);
return enabledActions;
}
}
The question is: How can I configure the dependency injection container in ASP.NET (using default option or Autofac) so that Func<Type, IActionRegistry> can be resolved?
For parameters in ActionRegistry<T> I guess I can do:
builder.RegisterAssemblyTypes().AsClosedTypesOf(typeof(BusinessAction<>));
builder.RegisterGeneric(typeof(Repository<>))
.As(typeof(IRepository<>))
.InstancePerLifetimeScope();
But, how can I configure Func<Type, IActionRegistry> so that I am able to automatically connect a request for Order with ActionRegistry<Order>? Is there a way to do that or I will need to manually configure the factory by writing some switch statement based on type (and how will that look)?
Is there a better way to achieve what I need here? The end goal is that once I have runtime type, I can get a list of business actions related to that type as well as a repository (so that I can fetch entity from DB).
What you're trying to do is possible, but it's not a common thing and isn't something magic you'll get out of the box. You'll have to write code to implement it.
Before I get to that... from a future perspective, you might get help faster and more eyes on your question if your repro is far more minimal. The whole BusinessAction<T> isn't really needed; the RequestHandler isn't needed... honestly, all you need to repro what you're doing is:
public interface IActionRegistry
{
}
public class ActionRegistry<T> : IActionRegistry
{
}
If the other stuff is relevant to the question, definitely include it... but in this case, it's not, so adding it in here just makes the question harder to read through and answer. I know I, personally, will sometimes just skip questions where there's a lot of extra stuff because there are only so many hours in the day, you know?
Anyway, here's how you'd do it, in working example form:
var builder = new ContainerBuilder();
// Register the action registry generic but not AS the interface.
// You can't register an open generic as a non-generic interface.
builder.RegisterGeneric(typeof(ActionRegistry<>));
// Manually build the factory method. Going from reflection
// System.Type to a generic ActionRegistry<Type> is not common and
// not directly supported.
builder.Register((context, parameters) => {
// Capture the lifetime scope or you'll get an exception about
// the resolve operation already being over.
var scope = context.Resolve<ILifetimeScope>();
// Here's the factory method. You can add whatever additional
// enhancements you need, like better error handling.
return (Type type) => {
var closedGeneric = typeof(ActionRegistry<>).MakeGenericType(type);
return scope.Resolve(closedGeneric) as IActionRegistry;
};
});
var container = builder.Build();
// Now you can resolve it and use it.
var factory = container.Resolve<Func<Type, IActionRegistry>>();
var instance = factory(typeof(DivideByZeroException));
Assert.Equal("ActionRegistry`1", instance.GetType().Name);
Assert.Equal("DivideByZeroException", instance.GetType().GenericTypeArguments[0].Name);
I make a lib that has some default controller. (e.g. HomeController,AccountController )
Then I create a asp.net core webapplicaion, and import that lib.
when I need to change action ( Home/Index ),I create a HomeController .Then run,and it throw AmbiguousMatchException.
I find a idea IActionConstraint, test project in Github
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class ControllerNamespaceConstraintAttribute : Attribute, IActionConstraint
{
public int Order => 1;
/// <inheritdoc />
public bool Accept(ActionConstraintContext context)
{
return IsValidForRequest(context.RouteContext, context.CurrentCandidate.Action);
}
public bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var actionNamespace = ((ControllerActionDescriptor)action).MethodInfo.DeclaringType.Namespace;
Console.WriteLine("IsValidForRequest:" + actionNamespace);
Console.WriteLine("routeContext.RouteData.DataTokens:" + routeContext.RouteData.DataTokens.Count());
if (routeContext.RouteData.DataTokens.ContainsKey("Namespace"))
{
var dataTokenNamespace = (string)routeContext.RouteData.DataTokens.FirstOrDefault(dt => dt.Key == "Namespace").Value;
return dataTokenNamespace == actionNamespace;
}
return true;
}
}
.......startup.cs
app.UseEndpoints(endpoints =>
{
var dataTokens = new RouteValueDictionary();
var ns = new[] { "TestMultipleController.Controllers" };
dataTokens["Namespaces"] = ns;
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}",
defaults: null,
constraints: null,
dataTokens: dataTokens);
});
..... HomeController.cs
[ControllerNamespaceConstraint]
public class HomeController : Controller
{
public IActionResult Index()
{
return Content(RouteData.DataTokens.Count.ToString());
}
}
but it also throw the same exception,too.
Debugger, I found it cannot get the DataTokens in IActionConstraint
then How can I do ?
As I understand you want the controller-action pair declared in the current project (the entry project) to override the controller-action pair declared in the class library.
This is a bit tricky and requires your certain design to handle this kind of ambiguity between at least 2 controller-action pairs (for the conventional routing). When it comes to design, it's hard to say everything in details so I assume that 2 controller-action pairs are considered as duplicates and would cause an ambiguity when selecting an action at runtime if they:
Have the same controller name (case-insensitive)
Have the same action name (case-insensitive)
Support one same http method (get, post, ...)
The action method's parameters don't matter (so e.g: Get will match Get(int id), ...).
Actually that's the logic the default action matching would use when finding the best matched action. I'm not so sure about the other criteria if any but the point here is to show you how to resolve this ambiguity by picking the best matched action (the one defined in the current executing assembly not in the class libraries).
So the idea here is to create your custom IApplicationModelConvention. The custom convention can help you apply some custom conventions affecting the loaded controllers, actions, routing ... With that convention, we have access to all ControllerModels which each one exposes its own set of valid ActionModels. So by removing the ActionModel that you don't want, you can resolve the ambiguity issue (because there is just one remaining ActionModel).
Here is the detail:
public class DuplicateActionsRemovalApplicationModelConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
var currentExecutingAsm = Assembly.GetExecutingAssembly();
var overriddenActions = application.Controllers.GroupBy(e => e.ControllerName)
//select only duplicate controllers
.Where(g => g.Count() > 1)
.SelectMany(g => g.SelectMany(e => e.Actions)
.GroupBy(o => o, ActionModelMatchingEqualityComparer.Instance)
//select only duplicate actions
.Where(k => k.Count() > 1)
.SelectMany(e => e.OrderByDescending(x => x.Controller.ControllerType.Assembly == currentExecutingAsm)
//select all except the action defined in the current executing assembly
.Skip(1)))
.Select(e => (controller: e.Controller, action: e));
//for each overridden action, just remove it from the owner controller
//this effectively means that the removed action will be overridden
//by the only remaining one (defined in the current executing assembly)
foreach(var overriddenAction in overriddenActions)
{
overriddenAction.controller.Actions.Remove(overriddenAction.action);
}
}
}
Here is the custom IEqualityComparer for ActionModel used in the code above. It helps match the 2 actions by the logic we described in the beginning.
public class ActionModelMatchingEqualityComparer : IEqualityComparer<ActionModel>
{
static readonly Lazy<ActionModelMatchingEqualityComparer> _instanceLazy =
new Lazy<ActionModelMatchingEqualityComparer>(() => new ActionModelMatchingEqualityComparer());
public static ActionModelMatchingEqualityComparer Instance => _instanceLazy.Value;
public bool Equals(ActionModel x, ActionModel y)
{
return string.Equals(x.ActionName, y.ActionName, StringComparison.OrdinalIgnoreCase) &&
x.Attributes.OfType<IActionHttpMethodProvider>().SelectMany(e => e.HttpMethods).DefaultIfEmpty("GET")
.Intersect(y.Attributes.OfType<IActionHttpMethodProvider>().SelectMany(e => e.HttpMethods).DefaultIfEmpty("GET"),
StringComparer.OrdinalIgnoreCase).Any();
}
public int GetHashCode(ActionModel obj)
{
return (obj.ActionName?.GetHashCode() ?? 0) ^
obj.Attributes.OfType<IActionHttpMethodProvider>()
.SelectMany(e => e.HttpMethods.Select(o => o.ToLower())).Distinct()
.OrderBy(e => e).Aggregate(0, (c, e) => (c * 13) ^ e.GetHashCode());
}
}
In the code above, we use the type IActionHttpMethodProvider which is implemented by all HttpMethodAttribute (e.g: HttpMethodAttribute itself, HttpPostAttribute, ...) to check if the 2 actions support at least one same http method.
Finally you can register your custom IApplicationModelConvention globally, like this (code for .net core 2.2):
//code in ConfigureServices method in Startup.cs
services.AddMvc(o => {
//...
o.Conventions.Add(new DuplicateActionsRemovalApplicationModelConvention());
//...
});
Now when you run your project, all the duplicate actions in the referenced class libraries will be removed and overridden by the one (considered as matched with the overridden actions) defined in the current executing assembly. Note that this current executing assembly is also the one containing the Startup.cs (the main/entry project) because that's where you add your custom convention DuplicateActionsRemovalApplicationModelConvention
Here is another approach using IActionSelector which is simpler. When it comes to selecting some things, the other answer of mine removes the things from which we select. This approach however will not remove the things but change how we select it. So basically the 2 approaches are fairly different in the idea. I believe that this IActionSelector may be better however:
public class CustomActionSelector : ActionSelector
{
public CustomActionSelector(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
ActionConstraintCache actionConstraintCache, ILoggerFactory loggerFactory) : base(actionDescriptorCollectionProvider, actionConstraintCache, loggerFactory)
{
}
protected override IReadOnlyList<ActionDescriptor> SelectBestActions(IReadOnlyList<ActionDescriptor> actions)
{
var executingAssembly = Assembly.GetExecutingAssembly();
var prioritizedActions = actions.GroupBy(e => e is ControllerActionDescriptor ca ? ca.ControllerName : "")
.SelectMany(g => g.Key == "" || g.Count() == 1 ? g :
g.GroupBy(e => (e as ControllerActionDescriptor).ActionName)
.SelectMany(o => o.Count() == 1 ? o :
o.OrderByDescending(e => (e as ControllerActionDescriptor).ControllerTypeInfo.Assembly == executingAssembly)
.Take(1))).ToList();
return base.SelectBestActions(prioritizedActions);
}
}
You need to register the custom action selector as a singleton like this:
services.AddSingleton<IActionSelector, CustomActionSelector>();
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.
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.
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();