Is there a way to centralise the model validation of the same property name across multiple DTOs?
For example, if I have the following classes to be used as the request body in a Web API action.
public class RegisterRequest
{
[Required]
[EmailAddress]
public string EmailAddress { get; set; } = null!;
[Required]
[MinLength(8)]
[RegularExpression(UserSettings.PasswordRegex)]
public string Password { get; set; } = null!;
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
}
public class UserProfileRequest
{
[Required]
public int UserId { get; set; }
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[Range(3, 3)]
public string? CCN3 { get; set; }
}
Can I centralise the attribute validation on DisplayName, duplicating the attributes goes against single responsibility principle. I believe I could achieve the centralised validation using an IFilterFactory and dropping the usage of attributes.
I opted to use a custom ActionFilterAttribute to achieve centralisation of the validation. The example below is for validating the country code (CCN3).
CountryCodeValidationAttribute.cs - custom attribute to be applied to properties (contains no logic)
[AttributeUsage(AttributeTargets.Property)]
public class CountryCodeValidationAttribute : Attribute
{
}
CountryCodeValidationActionFilter.cs - custom action filter that supports dependency injection and looks for the custom attribute on the properties. In my case I'm returning the standard invalid model bad request response.
public class CountryCodeValidationActionFilter : ActionFilterAttribute
{
private readonly ICountryService countryService;
private readonly IOptions<ApiBehaviorOptions> apiBehaviorOptions;
public CountryCodeValidationActionFilter(
ICountryService countryService,
IOptions<ApiBehaviorOptions> apiBehaviorOptions)
{
this.countryService = countryService;
this.apiBehaviorOptions = apiBehaviorOptions;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var actionArguments = context.ActionArguments;
foreach (var actionArgument in actionArguments)
{
if (actionArgument.Value == null) continue;
var propertiesWithAttributes = actionArgument.Value
.GetType()
.GetProperties()
.Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CountryCodeValidationAttribute)))
.ToList();
foreach (var property in propertiesWithAttributes)
{
var value = property.GetValue(actionArgument.Value)?.ToString();
if (value != null && await countryService.GetCountryAsync(value) != null) await next();
else
{
context.ModelState.AddModelError(property.Name, "Must be a valid country code");
context.Result = apiBehaviorOptions.Value.InvalidModelStateResponseFactory(context);
}
}
}
await base.OnActionExecutionAsync(context, next);
}
}
Program.cs - register the custom action filter.
builder.Services.AddMvc(options =>
{
options.Filters.Add(typeof(CountryCodeValidationActionFilter));
});
UserProfile.cs - apply the [CountryCodeValidation] attribute to the CountryCode property.
public class UserProfile
{
[Required]
[MaxLength(100)]
public string DisplayName { get; set; } = null!;
[CountryCodeValidation]
public string? CountryCode { get; set; }
}
I can take this same approach and apply it to the DisplayName property to create a centralised validation for it 👍.
I would like to build a authorization service using gRPC under .Net Code. In order to do that, I need to pass a System.Security.Claims.ClaimsPrincipal object as a request argument from caller to the server so the server can use it to authorize the caller. But I don't know how to that - how can I define a .proto for a class that is a standard library. What am I supposed to do?
I'm doing the same, using protobuf-net grpc libraries.
As many of the Identity/Security classes (if you are using them) are from Microsoft, you'll need to expose their members for serialization; you can use:
RuntimeTypeModel.Default.Add(typeof(SignInResult), false).Add(
nameof(SignInResult.Succeeded),
nameof(SignInResult.IsLockedOut),
nameof(SignInResult.IsNotAllowed),
nameof(SignInResult.RequiresTwoFactor)
);
and list the members that need to be exposed over gRpc.
As for ClaimsPrincipal, specifically, that is what I'm currently trying to implement. For Claims, i'm using a surrogate class:
RuntimeTypeModel.Default.Add(typeof(Claim), true).SetSurrogate(typeof(ClaimSurrogate));
public class ClaimSurrogate
{
[DataMember, ProtoMember(1)]
public string Type { get; set; }
[DataMember, ProtoMember(2)]
public ClaimsIdentity Subject { get; set; }
[DataMember, ProtoMember(3)]
public IDictionary<string, string> Properties { get; set; }
[DataMember, ProtoMember(4)]
public string OriginalIssuer { get; set; }
[DataMember, ProtoMember(5)]
public string Issuer { get; set; }
[DataMember, ProtoMember(6)]
public string ValueType { get; set; }
[DataMember, ProtoMember(7)]
public string Value { get; set; }
public static implicit operator ClaimSurrogate(Claim claim)
{
if (claim == null)
return null;
return new ClaimSurrogate()
{
Type = claim.Type,
Subject = claim.Subject,
Properties = claim.Properties,
OriginalIssuer = claim.OriginalIssuer,
Issuer = claim.Issuer,
ValueType = claim.ValueType,
Value = claim.Value
};
}
public static implicit operator Claim(ClaimSurrogate surrogate)
{
if (surrogate == null)
return null;
return new Claim(surrogate.Type, surrogate.Value, surrogate.ValueType, surrogate.Issuer, surrogate.OriginalIssuer, surrogate.Subject);
}
}
And I'm assuming that ClaimsPrincipal can be done the same way, but, I'm having trouble with it. That is how I came across your question...
Actually, by, trying to provide an answer...Literally, I just realized what I overlooked, I need to also set up a surrogate for the ClaimsIdentity
So far, I've needed surrogates for 'third' party classes that have get; only properties. ClaimsPrincipal has these types of properties, and so does ClaimsIdentity (as does Claim). I'll update/comment if ClaimsIdentitySurrogate does the trick
Updates:
Yes, it can be done. Surrogates, like the example above, will be needed for ClaimsIdentity and IIdentity. These classes are used as members/properties within ClaimsPrincipal.
ClaimsIdentity: you can mix up the SetSurrogate and the Add(nameof(...)) as it has get onlies and get/sets (get/sets go in the Add portion). Do not include the Actor in the ClaimsIdentity surrogate as it will create a never ending loop in your service's startup. If you do include it, make sure it is not a DataMember/Protomember. And (private) set it in the surrogate operator. Same same with Claims.
Essentially, any surrogates with members that reference the parent class, or that of a another type with a surrogate that references this parent type, will create a circular reference and error out your service on startup.
IIdentity: This is a simple one, just
RuntimeTypeModel.Default.Add(typeof(IIdentity), false).
Lastly (I posted this update when I thought I had it, but, amidst all the UT tests and changes, etc, I posted a bit early; after making a breaking change on the ClaimPrincipal surrogate class)....
You'll want an IIdentity dummy class that will be used in your ClaimPrincipal surrogate, instead of the IIdentity Identity {get;set;}. This dummy class should inherit from IIdentity, e.g.
[DataContract]
public class IIdentityFraud : System.Security.Principal.IIdentity
And within your surrogate's implicit operator:
IIdentityFraud identityfraud = null;
if (claimPrincipal.Identity != null)
{
identityfraud = new IIdentityFraud(claimPrincipal.Identity.AuthenticationType, claimPrincipal.Identity.Name, claimPrincipal.Identity.IsAuthenticated);
}
Updates (11/05/2021):
[DataContract]
public class ClaimsPrincipalSurrogate
{
[DataMember, ProtoMember(1)]
public IIdentityFraud Identity { get; set; }
[DataMember, ProtoMember(2)]
public IEnumerable<ClaimsIdentity> Identities { get; set; }
[DataMember, ProtoMember(3)]
public IEnumerable<Claim> Claims { get; set; }
public static implicit operator ClaimsPrincipalSurrogate(ClaimsPrincipal claimPrincipal)
{
if (claimPrincipal == null)
{
return null;
}
else
{
IIdentityFraud identityfraud = null;
if (claimPrincipal.Identity != null)
{
identityfraud = new IIdentityFraud(claimPrincipal.Identity.AuthenticationType, claimPrincipal.Identity.Name, claimPrincipal.Identity.IsAuthenticated);
}
return new ClaimsPrincipalSurrogate()
{
Identity = identityfraud, // (System.Security.Principal.IIdentity)identityfraud,
Identities = claimPrincipal.Identities,
Claims = claimPrincipal.Claims
};
}
}
public static implicit operator ClaimsPrincipal(ClaimsPrincipalSurrogate surrogate)
{
if (surrogate == null)
return null;
if (surrogate.Identities != null && surrogate.Identities.Any() == true)
{
return new ClaimsPrincipal(surrogate.Identities);
}
else if (surrogate.Identity != null)
{
return new ClaimsPrincipal(surrogate.Identity);
}
return new ClaimsPrincipal();
}
}
[DataContract]
public class ClaimsIdentitySurrogate
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; set; }
[DataMember, ProtoMember(2)]
public string Name { get; set; }
//[DataMember, ProtoMember(3)]
//public string Label { get; set; }
[DataMember, ProtoMember(4)]
public bool IsAuthenticated { get; set; }
[DataMember, ProtoMember(5)]
public IEnumerable<Claim> Claims { get; private set; }
//[DataMember, ProtoMember(6)]
//public object BootstrapContext { get; set; }
//[DataMember, ProtoMember(7)]
public ClaimsIdentity Actor { get; private set; }
[DataMember, ProtoMember(8)]
public string RoleClaimType { get; set; }
[DataMember, ProtoMember(9)]
public string NameClaimType { get; set; }
public static implicit operator ClaimsIdentitySurrogate(ClaimsIdentity claimIdentity)
{
if (claimIdentity == null)
return null;
return new ClaimsIdentitySurrogate()
{
AuthenticationType = claimIdentity.AuthenticationType,
Name = claimIdentity.Name,
//Label = claimIdentity.Label,
IsAuthenticated = claimIdentity.IsAuthenticated,
Claims = claimIdentity.Claims,
//BootstrapContext = claimIdentity.AuthenticationType,
Actor = claimIdentity.Actor,
RoleClaimType = claimIdentity.RoleClaimType,
NameClaimType = claimIdentity.NameClaimType
};
}
public static implicit operator ClaimsIdentity(ClaimsIdentitySurrogate surrogate)
{
if (surrogate == null)
{
return null;
}
if (surrogate.Claims?.Any() == true)
{
return new ClaimsIdentity(surrogate.Claims, surrogate.AuthenticationType);
}
else
{
return new ClaimsIdentity(surrogate.AuthenticationType, surrogate.NameClaimType, surrogate.RoleClaimType);
}
}
}
[DataContract]
public class IIdentityFraud : System.Security.Principal.IIdentity
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; private set; }
[DataMember, ProtoMember(2)]
public string Name { get; private set; }
[DataMember, ProtoMember(3)]
public bool IsAuthenticated { get; private set; }
public IIdentityFraud() { }
public IIdentityFraud(string authenticationType, string name, bool isAuthenticated)
{
this.AuthenticationType = authenticationType;
this.Name = name;
this.IsAuthenticated = isAuthenticated;
}
}
[DataContract] //don't know if this is really needed. Too involved in testing out the rest of it and have yet to come back to this.
public class IIdentitySurrogate : System.Security.Principal.IIdentity
{
[DataMember, ProtoMember(1)]
public string AuthenticationType { get; set; }
[DataMember, ProtoMember(2)]
public string Name { get; set; }
[DataMember, ProtoMember(3)]
public bool IsAuthenticated { get; set; }
public static implicit operator IIdentitySurrogate(IIdentityFraud iidentity)
{
if (iidentity == null)
return null;
return new IIdentitySurrogate()
{
AuthenticationType = iidentity.AuthenticationType,
Name = iidentity.Name,
IsAuthenticated = iidentity.IsAuthenticated
};
}
public static implicit operator IIdentityFraud(IIdentitySurrogate surrogate)
{
if (surrogate == null)
return null;
return new IIdentityFraud(surrogate.AuthenticationType, surrogate.Name, surrogate.IsAuthenticated);
}
}
More of what is executed on startups:
#region ClaimsIdentity
RuntimeTypeModel.Default.Add(typeof(ClaimsIdentity), true).Add(
nameof(ClaimsIdentity.Label),
nameof(ClaimsIdentity.BootstrapContext),
nameof(ClaimsIdentity.Actor)
).SetSurrogate(typeof(ClaimsIdentitySurrogate));
#endregion ClaimsIdentity
#region ClaimsPrincipal
RuntimeTypeModel.Default.Add(typeof(ClaimsPrincipal), true).SetSurrogate(typeof(ClaimsPrincipalSurrogate));
#endregion ClaimsPrincipal
#region IIdentity
RuntimeTypeModel.Default.Add(typeof(IIdentity), true);
#endregion IIdentity
This question already has answers here:
Inject Dependencies into Validation Attribute using ASP.NET Core's WebAPI
(4 answers)
Closed 1 year ago.
I want to make a custom validation attribute in .NET Core called CheckIfEmailExists. I want to make sure the user is not in my database already. So this is my create user view model:
public class CreateUserViewModel
{
public readonly UserManager userManager;
public CreateUserViewModel()
{
}
public ExtendedProfile ExtendedProfile { get; set; }
public User User { get; set; }
public int SchemeId { get; set; }
public SelectList Schemes { get; set; }
[Required]
[EmailAddress(ErrorMessage = "Invalid Email Address")]
[CheckIfEmailExists()]
[Display(Name = "Email Address")]
public string Email { get; set; }
[DataType(DataType.EmailAddress)]
[Display(Name = "Confirm Email Address")]
public string ConfirmEmail { get; set; }
}
Here is my custom validation:
public class CheckIfEmailExists : ValidationAttribute
{
private readonly UserManager _userManager;
public CheckIfEmailExists(UserManager userManager)
{
var _userManager = userManager;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var user = (User)validationContext.ObjectInstance;
var result = _userManager.FindByEmailAsync(user.Email).Result;
//do something
return ValidationResult.Success;
}
}
I get an error when I add my custom validation on my email property, the error is that I must pass in the usermanager object to the custom class constructor.
Why doesn't my app just inject the object itself?
Is there a way I can create a user manager object in my custom class without coupling the classes?
Should I only access my database in my controller?
The comment above from Nikki9696 helped me and now I know how to get my user manager and dbcontext in my custom validation attribute.
public class IsEmailInDatabase : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var context = (DatabaseContext)validationContext.GetService(typeof(DatabaseContext));
if (value == null)
{
return null;
}
else
{
var results = context.Users.AnyAsync(x => x.Email == value.ToString());
if (results.Result)
{
return new ValidationResult("This email address already exists in our database");
}
return null;
}
}
}
I'm not sure why while trying to create an entity which is 1:many
EF tries to add new entry in Asp Net Users instead of update 1:many
I have one user which has many items
SqlException: Violation of PRIMARY KEY constraint 'PK_AspNetUsers'. Cannot insert duplicate key in object 'dbo.AspNetUsers'. The duplicate key value is (cdbb1f2f-ddcf-40c0-97ec-f50f8049d87a).
public class Context : IdentityDbContext
{
public Context(DbContextOptions options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<File> Files { get; set; }
}
public class User : IdentityUser
{
public List<Item> Items { get; set; } = new List<Item>();
}
public class Item
{
private Item()
{
}
public Item(string title, User owner, File file)
{
Title = title;
Owner = owner;
File = file;
}
public int Id { get; private set; }
public string Title { get; set; }
public User Owner { get; set; }
public File File { get; set; }
public DateTime CreationDate { get; } = DateTime.Now;
}
And here's where's the problem:
var fileResult = await _file.SaveFile(input.File);
var item = new Item(input.Title, user, fileResult.File);
user.Items.Add(item);
await _context.Items.AddAsync(item);
await _context.SaveChangesAsync();
User is loaded with:
public User GetUser()
{
return _context.Users.FirstOrDefault(x => x.UserName == _http.HttpContext.User.Identity.Name);
}
I tried that:
When I change
public Item(string title, User owner, File file)
{
Title = title;
Owner = owner;
File = file;
}
to just:
public Item(string title, File file)
{
Title = title;
File = file;
}
and let it be handled by:
user.Items.Add(item);
then OwnerId in DB is null
Using:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.HasMany(x => x.Items)
.WithOne(x => x.Owner);
modelBuilder.Entity<Item>().HasOne(x => x.Owner);
}
is not helping either
The problem was casued by ServiceLifetime of DbContext
Because User was loaded in Controller and then thrawn into Service that was responsible for business logic
I changed
(options => options.UseSqlServer(Configuration["Database:ConnectionString"]), ServiceLifetime.Transient);
to
(options => options.UseSqlServer(Configuration["Database:ConnectionString"]), ServiceLifetime.Scoped);
and it works fine.
I am using custom uniqueemail confirmation attribute like
public class UniqueEmailAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
WMContext db = new WMContext();
var userWithTheSameEmail = db.Users.SingleOrDefault(
u => u.email == (string)value);
return userWithTheSameEmail == null;
}
}
It is working fine while inserting user. But, when I try to update user it gives an error.
public class User
{
public int userID { get; set; }
[Required]
[Display(Name = "Name")]
public string name { get; set; }
[Required]
[Display(Name = "Email")]
[UniqueEmail(ErrorMessage = "This email is used by another user")]
public string email { get; set; }
}
Solution : I have changed the IsValid method like this :
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var model = (User)validationContext.ObjectInstance;
WMContext db = new WMContext();
var userWithTheSameEmail = db.Users.SingleOrDefault(
u => u.email == (string)value && u.userID!=model.userID);
if (userWithTheSameEmail!=null)
{
return new ValidationResult("Bu eposta adresi kullanılıyor.");
}
return ValidationResult.Success;
}