So, we created this project: https://github.com/efonsecab/BlazorRestaurant
The EF Core logic for SaveChanges is extended to automatically fill the data for the auditing columns.
Logic is in the BlazorRestaurantDbContext.partial.cs file.
public partial class BlazorRestaurantDbContext
{
public override int SaveChanges()
{
ValidateAndSetDefaults();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
ValidateAndSetDefaults();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
ValidateAndSetDefaults();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ValidateAndSetDefaults();
return base.SaveChangesAsync(cancellationToken);
}
private void ValidateAndSetDefaults()
{
//Check https://www.bricelam.net/2016/12/13/validation-in-efcore.html
var entities = from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
string ipAddresses = String.Empty;
string assemblyFullName = String.Empty;
string rowCretionUser = String.Empty;
if (entities.Any(p => p is IOriginatorInfo))
{
ipAddresses = String.Join(",", GetCurrentHostIPv4Addresses());
assemblyFullName = System.Reflection.Assembly.GetEntryAssembly().FullName;
if (Thread.CurrentPrincipal != null && Thread.CurrentPrincipal.Identity != null)
rowCretionUser = Thread.CurrentPrincipal.Identity.Name;
else
rowCretionUser = "Unknown";
}
foreach (var entity in entities)
{
if (entity is IOriginatorInfo)
{
IOriginatorInfo entityWithOriginator = entity as IOriginatorInfo;
if (String.IsNullOrWhiteSpace(entityWithOriginator.SourceApplication))
{
entityWithOriginator.SourceApplication = assemblyFullName;
}
if (String.IsNullOrWhiteSpace(entityWithOriginator.OriginatorIpaddress))
{
entityWithOriginator.OriginatorIpaddress = ipAddresses;
}
if (entityWithOriginator.RowCreationDateTime == DateTimeOffset.MinValue)
{
entityWithOriginator.RowCreationDateTime = DateTimeOffset.UtcNow;
}
if (String.IsNullOrWhiteSpace(entityWithOriginator.RowCreationUser))
{
try
{
entityWithOriginator.RowCreationUser = rowCretionUser;
}
catch (Exception)
{
entityWithOriginator.RowCreationUser = "Unknown";
}
}
}
var validationContext = new ValidationContext(entity);
Validator.ValidateObject(
entity,
validationContext,
validateAllProperties: true);
}
}
public static List<string> GetCurrentHostIPv4Addresses()
{
//Check https://stackoverflow.com/questions/50386546/net-core-2-x-how-to-get-the-current-active-local-network-ipv4-address
// order interfaces by speed and filter out down and loopback
// take first of the remaining
var allUpInterfaces = NetworkInterface.GetAllNetworkInterfaces()
.OrderByDescending(c => c.Speed)
.Where(c => c.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
c.OperationalStatus == OperationalStatus.Up).ToList();
List<string> lstIps = new();
if (allUpInterfaces != null && allUpInterfaces.Count > 0)
{
foreach (var singleUpInterface in allUpInterfaces)
{
var props = singleUpInterface.GetIPProperties();
// get first IPV4 address assigned to this interface
var allIpV4Address = props.UnicastAddresses
.Where(c => c.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(c => c.Address)
.ToList();
allIpV4Address.ForEach((IpV4Address) =>
{
lstIps.Add(IpV4Address.ToString());
});
}
}
return lstIps;
}
}
We have used this same code in other apps and works, great.
In this specific app, however, the System.Threading.Thread.CurrentPrincipal and ClaimsPrincipal.Current, are null, when the ValidateAndSetDefaults method is executed.
Even tried the following
services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
options.Events.OnTokenValidated = (context) =>
{
System.Threading.Thread.CurrentPrincipal = context.Principal;
return Task.CompletedTask;
};
options.SaveToken = true;
});
The context.Principal does have all the data we need at this point, but we still get null reading the Thread's CurrenPrincipal object.
Any ideas how to fix it, so we can properly get the Authenticated User information in the code above, it exists in context.Principal, it is just not accesible later in the SaveChanges for EF Core.
Fixed by using Dependency Injection along with HttpContextAccesor
https://github.com/efonsecab/BlazorRestaurant/blob/main/src/BlazorRestaurantSln/BlazorRestaurant.DataAccess/Data/BlazorRestaurantDbContext.partial.cs
Related
public ServiceResponce Write(Guid senderID, Guid reciverID, string body)
{
Message message = new Message
{
Body = body
};
var reciver = context.Users.Where(c => c.Id == reciverID).Single();
var sender = context.Users.Where(c => c.Id == senderID).Single();
message.Sender = sender;
message.Reciver = reciver;
context.SaveChanges();
return new ServiceResponce();
}
I got exeption of empty sequence . I am geting Guid id resulsts from HTTPContext.Users.FindFirstValue(ClaimTypes.NameIdentifier)
and I am geting results it is not null.
I cant solve this problem.
Caller method :
public IActionResult Wright(Guid reciverID,string body)
{
var userID = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var neededID = Guid.Parse(userID);
_chatService.Write(neededID, reciverID, body);
return Ok();
}
Try this:
public ServiceResponce Write(...)
{
...
var reciver = context.Users.Where(c => c.Id == reciverID).SingleOrDefault();
var sender = context.Users.Where(c => c.Id == senderID).SingleOrDefault();
if(reciver == null || sender ==null)
{
//Log??
return null;
}
...
}
public IActionResult Wright(Guid reciverID,string body)
{
var userID = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
// If you really want to put the validation logic here
Guid neededID;
if (!Guid.TryParse(userId, out var neededID))
{
//Log?
return BadRequest();
}
if(_chatService.Write(neededID, reciverID, body) == null)
return BadRequest();
return Ok();
}
I have a product list query that I wrote under normal conditions. I have created a mapper for this query and I can use it as follows.
public IActionResult Index()
{
// use normal selecting
var productList = _context.Products.Select(s => new ProductDto
{
Id = s.Id,
CreateTime = s.CreateTime,
Title = s.Title,
Description = s.Description
}).ToList();
// use my mapper
var productListMap = _context.Products
.ToMap<Product, ProductDto>().ToList();
return View();
}
On simple data types it works just fine as long as the type and name are the same. But it cannot convert complex types (classes and models).
I thought it would be a nice idea to specify it as a parameter at the time of writing, so that it gets the job done. So I want a layout like below.
public IActionResult Index()
{
// what i'm doing now
var productList = _context.Products.Select(s => new ProductDto
{
Id = s.Id,
CreateTime = s.CreateTime,
Title = s.Title,
Description = s.Description,
Features = s.Features.ToMap<Feature, FeatureDto>().ToList(),
Images = s.Images.ToMap<Image, ImageDto>().ToList()
}).ToList();
// i want to do
var productListMap = _context.Products
.ToMap<Product, ProductDto>(p => p.Features, p.Images).ToList();
return View();
}
What do I need to change/add in the mapper class below to make it easy to use this way?
public static class Mapper
{
public static IEnumerable<TDestination> ToMap<TSource, TDestination>(this IEnumerable<TSource> sourceList)
{
return sourceList.Select(source => source.ToMap<TSource, TDestination>());
}
public static TDestination ToMap<TSource, TDestination>(this TSource source)
{
if (source == null) return default;
var entityProperties = source.GetType().GetProperties();
var dtoInstance = Activator.CreateInstance<TDestination>();
for (int i = 0; i < entityProperties.Length; i++)
{
var currentPropertyName = entityProperties[i].Name;
var value = GetPropertyValue(source, currentPropertyName);
if (dtoInstance.GetType().GetProperty(currentPropertyName) == null)
continue;
try { dtoInstance.GetType().GetProperty(currentPropertyName).SetValue(dtoInstance, value); }
catch (Exception ex) { /* Nothing */ }
}
return dtoInstance;
}
public static object GetPropertyValue(object source, string propName)
{
if (source == null) throw new ArgumentException("Value cannot be null.", nameof(source));
if (propName == null) throw new ArgumentException("Value cannot be null.", nameof(propName));
var prop = source.GetType().GetProperty(propName);
return prop != null ? prop.GetValue(source, null) : null;
}
}
I'm trying to validate an entity coming from an External context has not changed.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
I have a method which takes in an entity which has not been loaded from the context.
public bool Validate(Employee employee)
{
using (var context = new Context())
{
return context.Entry(employee).State == EntityState.Modified;
}
}
I would like to attach and verify that the attached entity is not modified from whats in the database.
I would prefer not to manually have to iterate of the properties. Is there a way to hack around this?
No need to attach the external entity. You can use the external entity to set values of the database entity and then check the state of the latter:
public bool Validate(Employee externalEmployee)
{
using var context = new Context(); // C# 8.0
var dbEntity = context.Where(x => x.Id == externalEmployee.Id).SingleOrDefault();
if (dbEntity != null)
{
context.Entry(dbEntity).CurrentValues.SetValues(externalEmployee);
return context.Entry(dbEntity).State == EntityState.Modified;
}
return false; // Or true, depending on your semantics.
}
You can try:
public static List<string> GetChanges<T>(this T obj, T dbObj)
{
List<string> result = new List<string>();
var type = typeof(T);
foreach (var prop in type.GetProperties())
{
var newValue = prop.GetValue(obj, null);
var dbValue = prop.GetValue(dbObj, null);
if(newValue == null && dbValue != null)
{
result.Add(prop.Name);
continue;
}
if (newValue != null && dbValue == null)
{
result.Add(prop.Name);
continue;
}
if (newValue == null && dbValue == null)
continue;
if (!newValue.ToString().Equals(dbValue.ToString()))
result.Add(prop.Name);
}
return result;
}
if resultList.Count > 0, your object has changes.
In your Validate Method:
public bool Validate(Employee employee)
{
using (var context = new Context())
{
Employee dbEmployee = context.Employee.Find(employee.Id);
if(employee.GetChanges(dbEmployee).Count > 0)
return true;
return false;
}
}
It's a god workaround =D
Works for me!
I have two tables. One of them is Customers which is in the database, the other one is ChangedCustomers, which comes from the user. I make some operations on ChangedCustomers table before sending the database so I use ChangedCustomersDTO. After these operations, I can not update the database.
public async Task<int> UpdateCustomers (IENumerable<ChangedCustomersDTO> changedCustomers
{
foreach(var item in changedCustomers)
{
var customer=_context.Customers.FirstOrDefault(x => x.CustomerId == item.CustomerId);
if(customer != null)
{
customer.Id=item.Id;
customer.Name=item.Name;
customer.Address=item.Address;
}
else
{
_context.Customers.Add(new Models.Customers()
{
customer.Id=new Guid();
customer.Name=item.Name;
customer.Address=item.Address;
}
}
}
return await _context.SaveChangesAsync();
}
All the data I have at the moment will be updated customers. They entered the if(customer !=null) but the method returns 0 and is not updated.
After comments I edit my code, but again returns zero.
public async Task<int> UpdateCustomers (IENumerable<ChangedCustomersDTO> changedCustomers
{
foreach(var item in changedCustomers)
{
var customer=_context.Customers.Where(x => x.CustomerId == item.CustomerId).FirstOrDefault();
if(customer != null)
{
customer.Name=item.Name;
customer.Address=item.Address;
}
else
{
_context.Customers.Add(new Models.Customers()
{
customer.Id=new Guid();
customer.Name=item.Name;
customer.Address=item.Address;
}
}
}
return await _context.SaveChangesAsync();
}
you have several bugs in your code. try this:
var customer=_context.Customers.Where(x => x.Id == item.Id).FirstOrDefault();
if(customer != null)
{
customer.Name=item.Name;
customer.Address=item.Address;
_context.Entry(customer).State=EntityState.Modified;
}
else
{
_context.Customers.Add(new Models.Customers
{
Id=new Guid(),
Name=item.Name,
Address=item.Address
}
}
I have this validator class:
internal class CustomerTypeValidator : AbstractValidator<CustomerType>
{
public CustomerTypeValidator()
{
RuleFor(x => x.Number).Must(BeANumber).WithState(x => CustomerTypeError.NoNumber);
}
private bool BeANumber(string number)
{
int temp;
bool ok = int.TryParse(number, out temp);
return ok && temp > 0;
}
}
And I have the service class:
public class CustomerTypeService
{
public CustomerType Save(CustomerType customerType)
{
ValidationResult results = Validate(customerType);
if (results != null && !results.IsValid)
{
throw new ValidationException<CustomerTypeError>(results.Errors);
}
//Save to DB here....
return customerType;
}
public bool IsNumberUnique(CustomerType customerType)
{
var result = customerTypeRepository.SearchFor(x => x.Number == customerType.Number).Where(x => x.Id != customerType.Id).FirstOrDefault();
return result == null;
}
public ValidationResult Validate(CustomerType customerType)
{
CustomerTypeValidator validator = new CustomerTypeValidator();
validator.RuleFor(x => x).Must(IsNumberUnique).WithState(x => CustomerTypeError.NumberNotUnique);
return validator.Validate(customerType);
}
}
However I get the following exception:
Property name could not be automatically determined for expression x => x. Please specify either a custom property name by calling 'WithName'.
Is the above not the correct way to add an extra rule?
With the current version of FluentValidation, it is possible to solve the above problem by doing the following:
public bool IsNumberUnique(CustomerType customerType, int id)
{
var result = customerTypeRepository.SearchFor(x => x.Number == customerType.Number).Where(x => x.Id != customerType.Id).FirstOrDefault();
return result == null;
}
public ValidationResult Validate(CustomerType customerType)
{
CustomerTypeValidator validator = new CustomerTypeValidator();
validator.RuleFor(x => x.Id).Must(IsNumberUnique).WithState(x => CustomerTypeError.NumberNotUnique);
return validator.Validate(customerType);
}