I have class User in my project and have model UserRow (for showing user in view)
it's UserRow
using System;
namespace Argussite.SupplierServices.ViewModels
{
public class UserRow
{
public Guid Id { get; set; }
public string FullName { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Status { get; set; }
public int Role { get; set; }
public Guid SupplierId { get; set; }
public bool ActionsAllowed { get; set; }
public bool MailResendRequired { get; set; }
}
}
and I need to add in my controller checking if ActionsAllowed
[HttpPost]
public ActionResult Unlock(Guid id)
{
var user = Context.Users.Find(id);
if (user == null)
{
return Json(CommandResult.Failure("User was not found. Please, refresh the grid and try again."));
}
var checkActionsAllowed = Context.Users.AsNoTracking()
.Select(e => new UserRow
{
Id = e.Id,
ActionsAllowed = e.ActionsAllowed
};
if (checkActionsAllowed == true)
{
user.Status = UserStatus.Active;
return Json(CommandResult.Success(string.Format("User {0} has been unlocked.", user.FullName)));
}
else return;
}
but I got error with ActionsAllowed = e.ActionsAllowed and
in else return;
Help me please to solve this problem.
You have two problems:
Context.Users.AsNoTracking()
.Select(e => new UserRow
{
ActionsAllowed = e.ActionsAllowed
};
returns a list of objects, not a single object.
You have queried the user above, so i guess you can write simply:
if (user.ActionsAllowed) {
user.Status = UserStatus.Active;
return Json(CommandResult.Success...);
}
The second problem is the return; statement.
Your method returns an action result, so you have to return something.
For example
return Json(CommandResult.Failure(
"ActionsAllowed = false"));
First error sounds like you User class doesn't provide a ActionsAllowed Boolean property, while the second error happens because you need to return something from the method that can be interpreted as an ActionResult.
EDIT:
Hmm, I didn't notice this the first time, but this:
var checkActionsAllowed = Context.Users.AsNoTracking()
.Select(e => new UserRow
{
Id = e.Id,
ActionsAllowed = e.ActionsAllowed
};
followed by this:
if (checkActionsAllowed == true)
makes no sense - you're not returning a boolean result from a Select method, but rather an IEnumerable. Perhaps you should add your User schema to your question so that it's more obvious what you're trying to accomplish.
Related
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 11 months ago.
Improve this question
I have some dtos returned by my API that have sensitive data fields like Createdby, CreatedDate, LastModifiedBy and LastModifiedDate. These fields should only be in the response if the user is authenticated and/or have allowed roles in his claims. My current running solution is to use my dto mappers that have a dependency on IUserIdentity (custom interface).
UserDto record
public record UserDto {
public string? CreatedBy { get; init; }
public DateTime? CreatedDate { get; init; }
public string? Email { get; set; }
public string? ExternalProviderUserId { get; set; }
public string? GivenName { get; set; }
public Guid? Id { get; set; }
public string? LastModifiedBy { get; init; }
public DateTime? LastModifiedDate { get; init; }
public string? Name { get; set; }
public string? Surname { get; set; }
}
UserDtoMapper class
public class UserDtoMapper : IUserDtoMapper {
private readonly IUserIdentity _userIdentity;
public UserDtoMapper(IUserIdentity userIdentity) {
_userIdentity = userIdentity;
}
public UserDto ToDto(User user) => new() {
CreatedBy = _userIdentity.IsAuthenticated ? user.CreatedBy : null,
CreatedDate = _userIdentity.IsAuthenticated ? user.CreatedDate : null,
Email = user.Email,
ExternalProviderUserId = user.ExternalProviderUserId,
GivenName = user.GivenName,
Id = user.Id,
LastModifiedBy = _userIdentity.IsAuthenticated ? user.LastModifiedBy : null,
LastModifiedDate = _userIdentity.IsAuthenticated ? user.LastModifiedDate : null,
Name = user.Name,
Surname = user.Surname
};
public List<UserDto> ToDtos(IEnumerable<User> users) {
return users.Select(o => ToDto(o)).ToList();
}
}
It work as entended but I would like to have a global and easier way to set these fields as senstitive and let the api filter them if the user is not authenticed and/or doesn't have allowed roles in his claims. After searching online for many days and did try and error solutions, I finally comes with my own solution and hope to have feedbacks on potential issues I could have. It works great so far.
My solution is to use the Filters in ASP.NET Core and Reflection (C#).
I've created this custom attribute SensitiveDataAttribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class SensitiveDataAttribute : Attribute {
public SensitiveDataAttribute() {
AllowedRoles = Array.Empty<string>();
}
public SensitiveDataAttribute(params string[] allowedRoles)
: this((IEnumerable<string>)allowedRoles) { }
public SensitiveDataAttribute(IEnumerable<string> allowedRoles) {
if (allowedRoles == null) {
throw new ArgumentNullException(nameof(allowedRoles));
}
if (!allowedRoles.Any()) {
throw new InvalidOperationException("At least one role must be specified.");
}
AllowedRoles = allowedRoles;
}
/// <summary>
/// Gets the collection of allowed roles.
/// </summary>
public IEnumerable<string> AllowedRoles { get; }
public Task<bool> IsValidAsync(ClaimsPrincipal user) {
bool isUserAuthenticated = user.Identity?.IsAuthenticated ?? false;
if (!isUserAuthenticated) {
return Task.FromResult(false);
}
if (!AllowedRoles.Any()) {
return Task.FromResult(true);
}
bool found = AllowedRoles.Any(r => user.IsInRole(r));
return Task.FromResult(found);
}
public override string ToString() {
if (!AllowedRoles.Any()) {
return $"{nameof(SensitiveDataAttribute)}:User must be authenticated";
}
string roles = string.Join("|", AllowedRoles);
var stringValue = $"User must be authenticated and User.IsInRole must be true for one of the following roles:({roles})";
return $"{nameof(SensitiveDataAttribute)}: {stringValue}";
}
}
And created this action filter SensitiveDataActionFilter
public class SensitiveDataActionFilter : IAsyncActionFilter {
private readonly ILogger<SensitiveDataActionFilter> _logger;
public SensitiveDataActionFilter(ILogger<SensitiveDataActionFilter> logger) {
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext _, ActionExecutionDelegate next) {
ActionExecutedContext executedContext = await next();
if (executedContext.Exception != null) {
return;
}
if (executedContext.Result is not OkObjectResult result) {
return;
}
if (result.Value == null) {
return;
}
_logger.LogDebug("Filtering action result sensitive data of type {Type} started.", result.Value.GetType());
Stopwatch stopwatch = Stopwatch.StartNew();
if (result.Value is IEnumerable elements) {
int index = 0;
foreach (var element in elements) {
await FilterResultAsync(executedContext, element, $"[{index++}].");
}
_logger.LogDebug("Filtering action result sensitive data of type {Type} finished in {Elapsed} ms.", result.Value.GetType(), stopwatch.ElapsedMilliseconds);
return;
}
await FilterResultAsync(executedContext, result.Value, null);
_logger.LogDebug("Filtering action result sensitive data of type {Type} finished in {Elapsed} ms.", result.Value.GetType(), stopwatch.ElapsedMilliseconds);
}
private async Task FilterResultAsync(ActionExecutedContext context, object? source, string? propPath) {
if (source == null) {
return;
}
PropertyInfo[] properties = source.GetType()
.GetProperties(BindingFlags.Instance
| BindingFlags.Public)
.Where(p => p.GetMethod != null
&& p.GetMethod.IsPublic
&& p.GetMethod.IsStatic == false)
.ToArray();
foreach (var propertyInfo in properties) {
if (propertyInfo == null) {
continue;
}
object? propertyValue = propertyInfo.GetValue(source);
string propertyName = propertyInfo.Name;
string path = $"{propPath}{propertyName}";
if (propertyValue == null) {
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
continue;
}
SensitiveDataAttribute? sensitiveDataAttribute = propertyInfo
.GetCustomAttribute<SensitiveDataAttribute>(true);
if (sensitiveDataAttribute != null) {
_logger.LogDebug("Property {Path} is sensitive: {Value}", path, propertyValue);
bool isValid = await sensitiveDataAttribute.IsValidAsync(context.HttpContext.User);
if (!isValid) {
_logger.LogDebug("Property {Path} to be cleared: {Reason}", path, sensitiveDataAttribute);
propertyInfo.SetValue(source, default);
continue;
}
}
if (propertyValue is DateTime or string) {
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
continue;
}
if (propertyValue is IEnumerable elements) {
int index = 0;
foreach (var element in elements) {
await FilterResultAsync(context, element, $"{path}.[{index++}].");
}
continue;
}
_logger.LogDebug("Property {Path}: {Value}", path, propertyValue);
await FilterResultAsync(context, propertyValue, $"{path}.");
}
}
}
And register the filter SensitiveDataActionFilter like this:
services.AddScoped<SensitiveDataActionFilter>();
services.AddControllers(options => options.Filters.AddService<SensitiveDataActionFilter>())
And add the attribute SensitiveDataAttribute to sensitive field:
public record UserDto {
[SensitiveData]
public string? CreatedBy { get; init; }
[SensitiveData]
public DateTime? CreatedDate { get; init; }
public string? Email { get; set; }
[SensitiveData("SYS_ADMIN")]
public string? ExternalProviderUserId { get; set; }
public string? GivenName { get; set; }
public Guid? Id { get; set; }
[SensitiveData]
public string? LastModifiedBy { get; init; }
[SensitiveData]
public DateTime? LastModifiedDate { get; init; }
public string? Name { get; set; }
public string? Surname { get; set; }
}
Circular reference is one problem I have have with my code. I might need to have a max-depth setting somewhere. I had problem with DateTime and string. Datetime gave me a circular reference issue and I needed to check if propertyValue is DateTime then stopped to go deeper. I needed to do the same with string because string implements IEnumerable.
Any feedbacks is appreciated. :)
My main argument against this solution is that in case of a bug you're revealing too much information. From a security point of view it much better to structure code so that in case of a bug not enough information is returned.
I have the following code in the controller and showing exception.
[HttpGet("{id}")]
public IActionResult GetCategoryGoalsById(int id)
{
try
{
var categories = _unitOfWork.Category.GetCategoryByGoalId(id);
if (categories == null)
{
_loggerManager.LogError($"Category with id: {id}, hasn't been found in db.");
return NotFound();
}
else
{
_loggerManager.LogInfo($"Returned category with id: {id}");
var categoryResult = _mapper.Map<CategoryDetailVm>(categories);
return Ok(categoryResult);
}
}
catch (Exception ex)
{
_loggerManager.LogError($"Something went wrong inside categoryResult action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}
Where is the entity class is like this:
public class Category
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string CategoryName { get; set; }
[ForeignKey(nameof(Goals))]
public int GoalId { get; set; }
public Goals Goals { get; set; }
}
and vm class for the module class written as:
public class CategoryDetailVm
{
public int Id { get; set; }
public string CategoryName { get; set; }
}
The code is written in repository pattern with UnitofWork and the repository part is written as:
public IEnumerable<Category> GetCategoryByGoalId(int goalId)
{
return FindByCondition(g => g.Goals.Id.Equals(goalId)).ToList();
}
following exception is showing here, how can I resolve the following problem:
ex {"Missing type map configuration or unsupported mapping.\r\n\r\nMapping types:\r\nObject ->
CategoryDetailVm\r\nSystem.Object ->
EthosAPI.ViewModelEntities.CategoryDetailVm"} System.Exception
{AutoMapper.AutoMapperMappingException}
It seems like you're missing an automapper mapping, did you add it?
https://docs.automapper.org/en/stable/Getting-started.html#how-do-i-use-automapper
eg. var config = new MapperConfiguration(cfg => cfg.CreateMap<CategoryDetailVm, Categorie>());
Also you're mapping an object to an entire list, so you should also have a mapping for lists, see:
https://docs.automapper.org/en/stable/Lists-and-arrays.html
So var categoryResult = _mapper.Map<CategoryDetailVm>(categories); should be more like var categoryResult = _mapper.Map<IEnumerable<CategoryDetailVm>>(categories); or something.
Here create an API to get the records, in my entity relation table there are twice start date and end date. Here my compulsion is one of them need to keep Null able type.
Here is ER that is SchoolCourses:
public class SchoolCourses
{
public Guid ID { get; set; }
public DateTime StartCourseDate { get; set; }
public DateTime EndCourseDate { get; set; }
public DateTime? StartSemDate { get; set; } // Null able type
public DateTime? EndSemDate { get; set; } // Null able type
}
I creates a repository for getting the value:
public async Task<ICollection<SchoolCourses>> GetcourseBySchoolId(Guid SchoolId)
{
List<SchoolCourses> schoolCourses = null;
schoolCourses = await _GpsContext.SchoolCourses.AsNoTracking()
.Where(x => x.SchoolsID == SchoolId)
.ToListAsync();
return schoolCourses;
}
And the Controller are like this:
public async Task<IActionResult> GetforSchoolCourse(string SchoolId)
{
var result = await _schoolCoursesRepository.GetcourseBySchoolId(Guid.Parse(SchoolId));
List<GetSchoolCourseBySchoolIdVm> getSchoolCourseBySchoolIdVms = new List<GetSchoolCourseBySchoolIdVm>();
foreach (SchoolCourses schoolCourse in result)
{
getSchoolCourseBySchoolIdVms.Add(new GetSchoolCourseBySchoolIdVm
{
id = schoolCourse.ID.ToString(),
StarCoursetDate = schoolCourse.StartCourseDate.ToString(),
EndCourseDate = schoolCourse.EndCourseDate.ToString(),
StartSemDate = schoolCourse.StartSemDate.ToString(),
EndSemDate = schoolCourse.EndSemDate.ToString(),
});
}
return Ok(getSchoolCourseBySchoolIdVms);
}
Here is View Model for reference:
public class GetSchoolCourseBySchoolIdVm
{
public string id { get; set; }
public string StarCoursetDate { get; set; }
public string EndCourseDate { get; set; }
public string StartSemDate { get; set; }
public string EndSemDate { get; set; }
}
After doing all the above staff it is getting exception error in swagger is following:
System.NullReferenceException: Object reference not set to an instance of an object.;
In your SchoolCourses model StartSemDate and EndSemDate are nullable types, so it must be possible that values of those fields are null. That should have been checked before using it, unlike you have used
StartSemDate = schoolCourse.StartSemDate.ToString(),
EndSemDate = schoolCourse.EndSemDate.ToString(),
here if any of the date is null then calling .ToString() method on it will throw NullReferenceException. Use safe navigation operator to check
schoolCourse.StartSemDate?.ToString()
or
schoolCourse.StartSemDate != null ? schoolCourse.StartSemDate.ToString() : string.Empty
Model
public partial class MemberModel
{
[Key]
public int MemberID { get; set; }
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name="Name")]
public string FullName { get { return string.Format(FirstName + " " + LastName); } }
[Required]
[StringLength(355)]
public string Address { get; set; }
[Required(ErrorMessage="The City field is Required")]
public int CityID { get; set; }
[Required(ErrorMessage = "The Country field is Required")]
public int CountryID { get; set; }
[Required]
[RegularExpression(#"^((0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{4}$|^\d{4}-\d{7}$", ErrorMessage = "Invalid Phone number")]
[Unique_Member]
public string Pin { get; set; }
[Display(Name="Mobile No.")]
[Required(ErrorMessage="Mobile No. Required")]
[RegularExpression(#"^((\+92)|(0092))-{0,1}\d{3}-{0,1}\d{7}$|^\d{11}$|^\d{4}-\d{7}$",ErrorMessage="Invalid Phone number")]
public string Phone { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
public virtual List<Order_SummeryModel> Order_Summeries { get; set; }
public virtual CountryModel Country { get; set; }
public virtual CityModel City { get; set; }
}
Custom Validation [Unique_Member]
its a custom validation for three properties "Pin","FirstName" and "LastName" which i made for create new member. It checks whether fullname and pin of new member is unique or not.
its works perfectly for create action but in edit action this restrict me to update the member model, i want to disable it for edit action, or there is another way to update the model with disable it.
public class Unique_MemberAttribute : ValidationAttribute
{
private static int count;
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Context_getway db = new Context_getway();
string membervalue = value.ToString();
//var count = db.Members.Where((x => x.Name == membervalue || x.Pin == membervalue || x.Email == membervalue)).Count();
var count_fname = db.Members.Where(x => x.FirstName == membervalue).Count();
var count_lname = db.Members.Where(x => x.LastName == membervalue).Count();
var count_pin = db.Members.Where(x => x.Pin == membervalue).Count();
if ((count_fname != 0)||(count_lname != 0))
{
count++;
if (count == 2)
{
return new ValidationResult("Member Already Exist with the same Full Name (Change First Name OR Last Name)!");
}
}
if (count_pin != 0)
{
return new ValidationResult("Member Already Exist with the same Pin!");
}
return ValidationResult.Success;
}
}
[MetadataType(typeof(MemberModel))]
public partial class MemberModel
{
}
Member Controller (edit action)
[HttpGet]
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
else
{
var member = db.Members.Find(id);
ViewBag.CountryID = new SelectList(db.CountryModels.ToList(), "CountryID", "Country",member.CountryID);
ViewBag.CityID = new SelectList(db.CityModels.ToList(), "CityID", "City",member.CityID);
if (member != null)
{
return View(member);
}
else
return HttpNotFound();
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(MemberModel member)
{
try
{
if (ModelState.IsValid)
{
db.Entry(member).State = System.Data.Entity.EntityState.Modified;
db.SaveChanges();
TempData["Msg"] = "Update Successfully";
return RedirectToAction("Index");
}
else
{
ViewBag.CountryID = new SelectList(db.CountryModels.ToList(), "CountryID", "Country",member.CountryID);
ViewBag.CityID = new SelectList(db.CityModels.ToList(), "CityID", "City",,member.CityID);
return View(member);
}
}
catch(Exception e)
{
TempData["Msg"] = "Update Unsuccessfully: "+ e.Message;
return View();
}
}
Try this:
ModelState.Remove("PropertyNameInModel");
You should still do the validation on Edit action method. Otherwise user can edit a record and select a unique combination already used by another record. You should simply use the Id property in your where clause to check any record other than the currently editing record.
So the first step is to get the Id property value of the current entity/view model you are validating. Then use the value in your where clauses.
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var idProperty = validationContext.ObjectType.GetProperty("MemberID");
var idValueObj = idProperty.GetValue(validationContext.ObjectInstance, null);
var id = 0;
if (idValueObj != null)
id = (int) idValueObj;
var db = new Context_getway();
string membervalue = value.ToString();
var count_fname = db.Members.Count(x => x.FirstName == membervalue && x.UserId!=id);
//Your existing code goes here. Make sure to use the id value in your WHERE clauses
}
I just hardcoded the property name ("MemberID") in the answer to give you the idea. But if you want more flexibility, you can pass that when you use the attribute as mentioned in this answer.
Also you should double check your conditions. I find issues with your code. What if the value of your count variable valus is more than 1(ex:2)), then your if (count == 2) will not return true (because your count is more than 2. I am not sure your business requirements/rules. But if you are looking for unique full names, you can create a single LINQ statement to do that ( Use Any method instead of getting Count as needed)
One way is to remove the error from ModelState in the Edit Controller action, right before checking if the model is valid.
But the better way is to separate your Edit and Insert models.
The Edit Model will have all validation rules for edit; and the Insert model will be inherited from the Edit model and overriding some properties with additional validation rules.
public partial class EditMemberModel
{
[Key]
public int MemberID { get; set; }
[Required]
[StringLength(255)] // Removed the Unique_Member rule**
[Display(Name = "First Name")]
public virtual string FirstName { get; set; }
/// etc.
}
public partial class InsertMemberModel : EditMemberModel
{
[Required]
[Unique_Member]
[StringLength(255)]
[Display(Name = "First Name")]
public override string FirstName { get; set; }
/// etc.
}
HI i have an MVC application which have CreatedDate and ModifiedDate feilds,
1. CreatedDate is when user create the module(any entry)
2. ModifiedDate is when user Edit the module
I have following Model class
namespace MyForms.Models
{
public class Master
{
public int ID { get; set; }
public string ModuleName { get; set; }
public int CreatedBy { get; set; }
public DateTime ? CreatedDate { get; set; }
public int ModifyBy { get; set; }
public DateTime ModifyDate { get; set; }
public Boolean IsActive { get; set; }
public Boolean IsDeleted { get; set; }
// public virtual ICollection<Master> MasterModules { get; set; }
}
public class MyFormDemoContext : DbContext
{
public DbSet<Master> MasterForms { get; set;}
}
}
Actions of Create and Edit
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Master master)
{
try
{
using (MyFormDemoContext context = new MyFormDemoContext())
{
master.CreatedBy = 1;
master.CreatedDate = DateTime.Now;
var a = master.CreatedDate;
master.IsActive = true;
master.ModifyBy = 1;
master.ModifyDate = DateTime.Now;
master.IsDeleted = false;
context.MasterForms.Add(master);
context.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
public ActionResult Edit(int id)
{
using (MyFormDemoContext context = new MyFormDemoContext())
{
return View(context.MasterForms.Find(id));
}
}
//
// POST: /Home/Edit/5
[HttpPost]
public ActionResult Edit(int id, Master valpara)
{
try
{
using (MyFormDemoContext context = new MyFormDemoContext())
{
valpara.CreatedBy = 1;
valpara.CreatedDate = DateTime.Now;
valpara.IsActive = true;
valpara.ModifyBy = 1;
valpara.ModifyDate = DateTime.Now;
valpara.IsDeleted = false;
valpara.ModifyDate = DateTime.Now;
context.Entry(valpara).State = System.Data.EntityState.Modified;
context.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
} }
1.currently when i create the module(entry) createdDate goes as current date
2. When i edit the module, modifiedDate and createdDate goes same
My expections
I want the createdDate Remains same when i Modify or edit the entry only modified date will be updated
When i edit the module, modifiedDate and createdDate goes same
Well, that's because in your Edit action you are specifically setting the CreatedDate, remove this line
valpara.CreatedDate = DateTime.Now
and only the ModifiedDate will be updated. However, a better approach would be to have your DB configured to set the date automatically (e.g. if you are using MSSQL set the default value to GetUtcDate()) and have EF pull that value instead of setting it client-side.
You need to set DatabaseGeneratedOption.Identity on that particular field which tells EF that the DB will generate the value.
FYI - you should really consider storing your dates as UTC rather than local i.e. use DateTime.UtcNow rather than DateTime.Now.
As well as the above, in your Edit you are actually re-creating a new entry each time. If you want to modify an existing record then you need to pull that record out of the DB first e.g.
using (MyFormDemoContext context = new MyFormDemoContext())
{
var record = context.MasterForms.SingleOrDefault(x => x.ID == id);
if (record != null)
{
record.ModifyBy = 1;
record.ModifyDate = DateTime.UtcNow;
context.SaveChanges();
}
}