How to create audit log / audit trail in asp.net mvc - audit

When we are used code first or entity framework then there is easiest way to audit trail the actions like add , update and delete.

Create a class for capture the changes or track the changes when entity added, modifies or deleted.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Web;
namespace MVC_AuditTrail.Models
{
public class AuditTrailFactory
{
private DbContext context;
public AuditTrailFactory(DbContext context)
{
this.context = context;
}
public Audit GetAudit(DbEntityEntry entry)
{
Audit audit = new Audit();
// var user = (User)HttpContext.Current.Session[":user"];
audit.UserId = "swapnil";// user.UserName;
audit.TableName = GetTableName(entry);
audit.UpdateDate = DateTime.Now;
audit.TableIdValue = GetKeyValue(entry);
//entry is Added
if (entry.State == EntityState.Added)
{
var newValues = new StringBuilder();
SetAddedProperties(entry, newValues);
audit.NewData = newValues.ToString();
audit.Actions = AuditActions.I.ToString();
}
//entry in deleted
else if (entry.State == EntityState.Deleted)
{
var oldValues = new StringBuilder();
SetDeletedProperties(entry, oldValues);
audit.OldData = oldValues.ToString();
audit.Actions = AuditActions.D.ToString();
}
//entry is modified
else if (entry.State == EntityState.Modified)
{
var oldValues = new StringBuilder();
var newValues = new StringBuilder();
SetModifiedProperties(entry, oldValues, newValues);
audit.OldData = oldValues.ToString();
audit.NewData = newValues.ToString();
audit.Actions = AuditActions.U.ToString();
}
return audit;
}
private void SetAddedProperties(DbEntityEntry entry, StringBuilder newData)
{
foreach (var propertyName in entry.CurrentValues.PropertyNames)
{
var newVal = entry.CurrentValues[propertyName];
if (newVal != null)
{
newData.AppendFormat("{0}={1} || ", propertyName, newVal);
}
}
if (newData.Length > 0)
newData = newData.Remove(newData.Length - 3, 3);
}
private void SetDeletedProperties(DbEntityEntry entry, StringBuilder oldData)
{
DbPropertyValues dbValues = entry.GetDatabaseValues();
foreach (var propertyName in dbValues.PropertyNames)
{
var oldVal = dbValues[propertyName];
if (oldVal != null)
{
oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
}
}
if (oldData.Length > 0)
oldData = oldData.Remove(oldData.Length - 3, 3);
}
private void SetModifiedProperties(DbEntityEntry entry, StringBuilder oldData, StringBuilder newData)
{
DbPropertyValues dbValues = entry.GetDatabaseValues();
foreach (var propertyName in entry.OriginalValues.PropertyNames)
{
var oldVal = dbValues[propertyName];
var newVal = entry.CurrentValues[propertyName];
if (oldVal != null && newVal != null && !Equals(oldVal, newVal))
{
newData.AppendFormat("{0}={1} || ", propertyName, newVal);
oldData.AppendFormat("{0}={1} || ", propertyName, oldVal);
}
}
if (oldData.Length > 0)
oldData = oldData.Remove(oldData.Length - 3, 3);
if (newData.Length > 0)
newData = newData.Remove(newData.Length - 3, 3);
}
public long? GetKeyValue(DbEntityEntry entry)
{
var objectStateEntry = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
long id = 0;
if (objectStateEntry.EntityKey.EntityKeyValues != null)
id = Convert.ToInt64(objectStateEntry.EntityKey.EntityKeyValues[0].Value);
return id;
}
private string GetTableName(DbEntityEntry dbEntry)
{
TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), false).SingleOrDefault() as TableAttribute;
string tableName = tableAttr != null ? tableAttr.Name : dbEntry.Entity.GetType().Name;
return tableName;
}
}
public enum AuditActions
{
I,
U,
D
}
}
Then create audit table entity and context class.
And Override savechanges method in this method get audit changes and save before base entity saved.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Web;
namespace MVC_AuditTrail.Models
{
public class Student
{
public int StudentID { get; set; }
public string Name { get; set; }
public string mobile { get; set; }
}
public class Audit
{
public long Id { get; set; }
public string TableName { get; set; }
public string UserId { get; set; }
public string Actions { get; set; }
public string OldData { get; set; }
public string NewData { get; set; }
public Nullable<long> TableIdValue { get; set; }
public Nullable<System.DateTime> UpdateDate { get; set; }
}
public class StdContext : DbContext
{
private AuditTrailFactory auditFactory;
private List<Audit> auditList = new List<Audit>();
private List<DbEntityEntry> objectList = new List<DbEntityEntry>();
public StdContext() : base("stdConnection")
{
Database.SetInitializer<StdContext>(new CreateDatabaseIfNotExists<StdContext>());
}
public DbSet<Student> Student { get; set; }
public DbSet<Audit> Audit { get; set; }
public override int SaveChanges()
{
auditList.Clear();
objectList.Clear();
auditFactory = new AuditTrailFactory(this);
var entityList = ChangeTracker.Entries().Where(p => p.State == EntityState.Added || p.State == EntityState.Deleted || p.State == EntityState.Modified);
foreach (var entity in entityList)
{
Audit audit = auditFactory.GetAudit(entity);
bool isValid = true;
if (entity.State == EntityState.Modified && string.IsNullOrWhiteSpace(audit.NewData) && string.IsNullOrWhiteSpace(audit.OldData))
{
isValid = false;
}
if (isValid)
{
auditList.Add(audit);
objectList.Add(entity);
}
}
var retVal = base.SaveChanges();
if (auditList.Count > 0)
{
int i = 0;
foreach (var audit in auditList)
{
if (audit.Actions == AuditActions.I.ToString())
audit.TableIdValue = auditFactory.GetKeyValue(objectList[i]);
this.Audit.Add(audit);
i++;
}
base.SaveChanges();
}
return retVal;
}
}
}

Related

Add <a> link for every node in JTree using Asp.Net Core

how i can add a tag link to every node(root or children) in JTree, I Fetch data from database with EFCore
and i want to every node have link like this:
<a class="btn btn-primary" asp-action="TaskTypeDetail" asp-controller="Admin" asp-route-TaskTypeNumber=#item.TaskTypeNumber>details</a>
mycontroller like this :
public class TreeviewController : Controller
{
private readonly RoleManager<IdentityRole> roleManager;
private readonly UserManager<ApplicationUser> userManager;
private readonly ApplicationDbContext applicationDbContext;
public TreeviewController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager, ApplicationDbContext applicationDbContext)
{
this.roleManager = roleManager;
this.userManager = userManager;
this.applicationDbContext = applicationDbContext;
}
public IActionResult Index()
{
return View();
}
public JsonResult GetRoot()
{
List<JsTreeModel> items = GetTree();
return new JsonResult ( items );
}
public JsonResult GetChildren(string id)
{
List<JsTreeModel> items = GetTree(id);
return new JsonResult ( items );
}
public List<JsTreeModel> GetTree()
{
bool checkchildren;
var items = new List<JsTreeModel>();
foreach (var role in roleManager.Roles)
{
foreach (var user in userManager.Users)
{
checkchildren = false;
var checkUserInRole = userManager.IsInRoleAsync(user, role.Name).Result;
if (checkUserInRole)
{
var checkEmployeeForUser = applicationDbContext.EmployeeInRoles.Where(s => s.RoleId == role.Id).ToList();
if (checkEmployeeForUser.Count > 0)
{
checkchildren = true;
}
items.Add(new JsTreeModel { id = user.Id.ToString(), parent = "#", text = user.Name+" "+user.Family+" ریشه " , children = checkchildren,a_attr=""});
}
}
}
// set items in here
return items;
}
public List<JsTreeModel> GetTree(string id)
{
var items = new List<JsTreeModel>();
// set items in here
//Loop and add the Child Nodes.
bool checkchildren;
string Role="";
foreach (var subType in applicationDbContext.EmployeeInRoles)
{
checkchildren = false;
string Parentid = "";
foreach (var findParent in userManager.Users)
{
var roleid = roleManager.FindByIdAsync(subType.RoleId);
var checkParent = userManager.IsInRoleAsync(findParent, roleid.Result.Name).Result;
if (checkParent) {
Parentid = findParent.Id;
}
}
var user = userManager.Users.SingleOrDefault(s => s.Id == subType.UserId);
foreach(var role in roleManager.Roles)
{
var checkUserInRole = userManager.IsInRoleAsync(user, role.Name).Result;
if (checkUserInRole)
{
Role = role.Id;
break;
}
}
var checkEmployeeForUser = applicationDbContext.EmployeeInRoles.Where(s => s.RoleId == Role).ToList();
if (checkEmployeeForUser.Count > 0)
{
checkchildren = true;
}
items.Add(new JsTreeModel { id = subType.Id.ToString(), parent = Parentid, text = user.Name + " " + user.Family, children=checkchildren});
}
return items;
}
}
JsTreeModel like this:
public class JsTreeModel
{
public string id { get; set; }
public string parent { get; set; }
public string text { get; set; }
public string a_attr { get; set; }
public bool children { get; set; } // if node has sub-nodes set true or not set false
}
and Index.cshtml
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<div id='treeview'></div>
<script>
$('#treeview').jstree({
"plugins": ["search","contextmenu"],
'core': {
'data': {
'url': function (node) {
return node.id === '#' ? "/Treeview/GetRoot" : "/Treeview/GetChildren/" + node.id;
},
'data': function (node) {
return {'id': node.id };
}
}
}
});
$('#treeview').on('changed.jstree', function (e, data) {
console.log("=> selected node: " + data.node.id);
});
but every node that show me, when I click on it, doesn't have link and # instead of link
can any one help me ?

Why I can only update a user's data in ASP.Net Core with Identity?

I was testing my application and I saw that I can only update the information of a single user and not that of the others, the application does not give me an error message or anything, it only reloads the page and the data does not reach the database data, whereas with a single user this does not happen.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UniJobs.Data;
using UniJobs.Models;
namespace UniJobs.Areas.Identity.Pages.Account.Manage
{
public partial class IndexModel : PageModel
{
private readonly UserManager<Usuarios> _userManager;
private readonly SignInManager<Usuarios> _signInManager;
private readonly ApplicationDbContext _context;
private readonly IWebHostEnvironment _webHostEnvironment;
public IndexModel(
UserManager<Usuarios> userManager,
SignInManager<Usuarios> signInManager,
ApplicationDbContext context,
IWebHostEnvironment webHostEnvironment)
{
_userManager = userManager;
_signInManager = signInManager;
this._context = context;
this._webHostEnvironment = webHostEnvironment;
}
public string Username { get; set; }
[TempData]
public string StatusMessage { get; set; }
[BindProperty]
public InputModel Input { get; set; }
public class InputModel
{
[Display(Name = "Imagen de perfil")]
public IFormFile InputImagen { get; set; }
[Display(Name = "¿Quién Soy?")]
public string QuienSoy { get; set; }
[Display(Name = "Sobre mis habilidades")]
public string SobreHabilidades { get; set; }
[Display(Name = "Sobre mis aptitudes")]
public string SobreAptitudes { get; set; }
[Required]
[DataType(DataType.Text)]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[Display(Name = "Provincia")]
public int? Provincia { get; set; }
[Display(Name = "Mi universidad")]
public int? MiUni { get; set; }
[Required]
[DataType(DataType.Text)]
[Display(Name = "Apellido")]
public string LastName { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Fecha de nacimiento")]
public DateTime? BirthDate { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
private async Task LoadAsync(Usuarios user)
{
var userName = await _userManager.GetUserNameAsync(user);
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
user.ID_Uni = _context.ListaEstudiantesUniversidades.Where(U => U.FK_UsuarioID == user.Id)
.Select(U => U.FK_UniversidadID).FirstOrDefault();
Username = userName;
Input = new InputModel
{
FirstName = user.FirstName,
LastName = user.LastName,
BirthDate = user.BirthDate,
Provincia = user.FK_ProvinciaID,
PhoneNumber = phoneNumber,
MiUni = user.ID_Uni,
QuienSoy = user.QuienSoy,
SobreHabilidades = user.SobreHabilidades,
SobreAptitudes = user.SobreAptitudes
};
}
public async Task<IActionResult> OnGetAsync()
{
ViewData["FK_ProvinciaID"] = new SelectList(_context.Provincias, "ProvinciasID", "Provincia");
ViewData["Universidad"] = new SelectList(_context.Universidades, "UniversidadesID", "NombreUniversidad");
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await LoadAsync(user);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!ModelState.IsValid)
{
await LoadAsync(user);
return Page();
}
var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
StatusMessage = "Unexpected error when trying to set phone number.";
return RedirectToPage();
}
}
if (Input.InputImagen != null)
{
//Save image to wwwroot/image
string wwwRootPath = _webHostEnvironment.WebRootPath;
string ImageName = Path.GetFileNameWithoutExtension(Input.InputImagen.FileName);
string ExtensionImage = Path.GetExtension(Input.InputImagen.FileName);
user.ProfileImage = ImageName = ImageName + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ExtensionImage;
string path = Path.Combine(wwwRootPath + "/UsersImages/Users/", ImageName);
using (var fileStream = new FileStream(path, FileMode.Create))
{
await Input.InputImagen.CopyToAsync(fileStream);
}
//Insert record
}
if (Input.MiUni != _context.ListaEstudiantesUniversidades.Where(U => U.FK_UsuarioID == user.Id)
.Select(U => U.FK_UniversidadID).FirstOrDefault())
{
var UniID = _context.ListaEstudiantesUniversidades.Where(U => U.FK_UsuarioID == user.Id)
.Select(U => U.ListaEstudiantesUniversidadesID).FirstOrDefault();
var StudentUni = await _context.ListaEstudiantesUniversidades.FindAsync(UniID);
_context.ListaEstudiantesUniversidades.Remove(StudentUni);
await _context.SaveChangesAsync();
var Uni = _context.ListaEstudiantesUniversidades.Add(new ListaEstudiantesUniversidades
{
FK_UniversidadID = Input.MiUni.Value,
FK_UsuarioID = user.Id
});
await _context.SaveChangesAsync();
}
if (Input.FirstName != user.FirstName)
{
user.FirstName = Input.FirstName;
}
if (Input.LastName != user.LastName)
{
user.LastName = Input.LastName;
}
if (Input.BirthDate != user.BirthDate)
{
user.BirthDate = Input.BirthDate;
}
if (Input.Provincia != user.FK_ProvinciaID)
{
user.FK_ProvinciaID = Input.Provincia;
}
if (Input.QuienSoy != user.QuienSoy)
{
user.QuienSoy = Input.QuienSoy;
}
if (Input.SobreHabilidades != user.SobreHabilidades)
{
user.SobreHabilidades = Input.SobreHabilidades;
}
if (Input.SobreAptitudes != user.SobreAptitudes)
{
user.SobreAptitudes = Input.SobreAptitudes;
}
await _userManager.UpdateAsync(user);
await _signInManager.RefreshSignInAsync(user);
StatusMessage = "Your profile has been updated";
return RedirectToPage();
}
}
}
This is the code of the Identity Index class

Blazor Server-Side Recursive Component over-memory consumption

I have this ChainableSelect component which is select and based on that it generates another ChainableSelect and on and on.
I noticed that if i changed the selection like around 7~10 times
memory consumption starts to rise without stopping causing my laptop to freeze
To Reproduce
Steps to reproduce the behavior:
Using this version of ASP.NET Core '3.0' release
Run this code
public class ChainableSelect<TEntity> : InputableBaseComponent where TEntity : DbModel
{
[Parameter]
public IChainableSelectDataFetcher<TEntity> DataFetcher { get; set; }
[Parameter]
public string FilterText { get; set; } = "";
[Parameter]
public bool IsBusy { get; set; }
[Parameter]
public bool FilterChanged { get; set; } = true;
[Parameter]
public string ParentPropertyName { get; set; }
[Parameter]
public TEntity ParentEntity { get; set; }
TEntity _localParentEntity;
IViewProperty _localViewProperty;
ReadOnlyObservableCollection<TEntity> _entities { get; set; }
bool _updateEntities = true;
protected override void OnInitialized()
{
base.OnInitialized();
_localViewProperty = ViewPropertyFactory.CreateEmptyProperty("", null);
}
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (DataFetcher != null)
{
if (_localParentEntity != ParentEntity || ParentEntity == null || FilterChanged)
{
_entities = await DataFetcher.GetDescendents(ParentEntity, ParentPropertyName);
_updateEntities = false;
}
_localViewProperty.PropertyValue = null;
FilterChanged = true;
StateHasChanged();
}
_localParentEntity = ParentEntity;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (_entities == null)
return;
if (_entities.Count == 0)
return;
var seq = 0;
base.BuildRenderTree(builder);
builder.OpenElement(seq, "input");
builder.AddAttribute(seq, "class", "form-control form-control-sm dropdown-input bg-white");
builder.AddAttribute(seq, "value", _localViewProperty.PropertyValue ?? "--SELECT--");
builder.AddAttribute(seq, "readonly", "readonly");
builder.AddAttribute(seq, "onfocus", "document.getElementById('" + Id + "').style.zIndex=1000");
builder.CloseElement();
builder.OpenElement(++seq, "div");
builder.AddAttribute(seq, "class", "dropdown-div border");
builder.AddAttribute(seq, "onmouseover", "document.getElementById('" + Id + "').style.zIndex=1000");
builder.AddAttribute(seq, "onmouseout", "document.getElementById('" + Id + "').style.zIndex=1");
builder.OpenElement(++seq, "div");
builder.AddAttribute(seq, "class", "p-1 m-1 bg-white");
builder.OpenElement(++seq, "input");
builder.AddAttribute(seq, "class", "form-control form-control-sm");
builder.AddAttribute(seq, "oninput", new Action<ChangeEventArgs>(OnFilterChanged));
builder.CloseElement();
builder.OpenElement(++seq, "hr");
builder.CloseElement();
builder.CloseElement();
builder.OpenElement(++seq, "a");
builder.AddAttribute(seq, "onclick", new Action(() => OnSelect(null)));
builder.AddContent(seq, "None");
builder.CloseElement();
if (_entities != null)
foreach (var _entity in _entities)
{
if (!_entity.ToString().Contains(FilterText, StringComparison.InvariantCultureIgnoreCase))
continue;
builder.OpenElement(++seq, "a");
builder.AddAttribute(seq, "onclick", new Action(() => OnSelect(_entity)));
if (_entity == _localViewProperty.PropertyValue)
builder.AddAttribute(seq, "class", "selected");
builder.AddContent(seq, _entity.ToString());
builder.CloseElement();
}
builder.CloseElement();
builder.CloseElement();
FilterChanged = false;
if (_localViewProperty.PropertyValue != null && _localViewProperty.PropertyValue != ParentEntity)
{
builder.OpenComponent<ChainableSelect<TEntity>>(++seq);
builder.AddAttribute(seq, "Parameters", Parameters);
builder.AddAttribute(seq, "ParentEntity", ViewProperty.PropertyValue);
builder.AddAttribute(seq, "ParentPropertyName", ParentPropertyName);
builder.CloseComponent();
}
}
private async void OnSelect(TEntity entity)
{
ViewProperty.PropertyValue = entity;
_localViewProperty.PropertyValue = entity;
FilterChanged = true;
_updateEntities = false;
StateHasChanged();
}
protected override bool ShouldRender()
{
return !IsBusy && FilterChanged;
}
CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private async void OnFilterChanged(ChangeEventArgs e)
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
var _cancelationToken = _cancellationTokenSource.Token;
IsBusy = true;
await Task.Delay(200, _cancelationToken).ContinueWith(t =>
{
if (!_cancelationToken.IsCancellationRequested)
{
FilterText = e.Value.ToString();
FilterChanged = true;
}
IsBusy = false;
InvokeAsync(StateHasChanged);
});
}
}
ChainableDataFetcher
public class ChainableEntityDataFetcher<TEntity> : IChainableSelectDataFetcher<TEntity> where TEntity : DbModel
{
public Func<TEntity, string, Task<ReadOnlyObservableCollection<TEntity>>> GetDescendents { get; set; }
private readonly IDataAccessLayer<TEntity> _dataAccessLayer;
private PropertyInfo _parentProperty;
public ChainableEntityDataFetcher(IDataAccessLayer<TEntity> dataAccessLayer)
{
_dataAccessLayer = dataAccessLayer;
GetDescendents = DataFetcherAsync;
}
public void Dispose()
{
_dataAccessLayer.Dispose();
GetDescendents = null;
_parentProperty = null;
}
async Task<ReadOnlyObservableCollection<TEntity>> DataFetcherAsync(TEntity parentEntity,string parentPropertyName)
{
var _comparableValue = parentEntity == null ? " is NULL" : " = " + parentEntity.DbModelId.ToString();
var _dataList = _dataAccessLayer.GetEntities(new Dapper.CommandDefinition(String.Format("select * from {0} where {1}{2}",
_dataAccessLayer.TableName,
parentPropertyName,
_comparableValue)));
ObservableCollection<TEntity> _entities = new ObservableCollection<TEntity>(_dataList);
ReadOnlyObservableCollection<TEntity> _readOnlyEntites = new ReadOnlyObservableCollection<TEntity>(_entities);
return _readOnlyEntites;
}
}
DataAccessLayer
public class DataAccessLayer<TEntity> : IDataAccessLayer<TEntity> where TEntity : DbModel
{
public IDbConnectionBuilder DbConnectionBuilder { get; }
private readonly string _tableName;
...
public ICollection<TEntity> GetEntities(CommandDefinition command)
{
using (var _connection = DbConnectionBuilder.GetDbConnection())
{
_connection.Open();
return _connection.Query<TEntity>(command).ToList();
}
}
...
Screenshots
I found the problem..
It is with the async StateHasChanged
Just don’t use StateHasChanges in an async operation

Different behaviour between bridging and standalone model/class

We are trying to streamline our automatic updating fields for DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy and worked OK by adding routine OnBeforeSaving in the ApplicationDBContext.cs and we also do the SOFT-DELETE for retaining the records (flagged as IsDeleted approach instead) when we deleted:
ApplicationDBContext.cs -
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
namespace AthlosifyWebArchery.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
IHttpContextAccessor httpContextAccessor
)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }
public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }
public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }
public virtual DbSet<AthlosifyWebArchery.Models.Club> Club { get; set; }
public DbSet<AthlosifyWebArchery.Models.ClubApplicationUser> ClubApplicationUser { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
foreach (var entityType in builder.Model.GetEntityTypes())
{
// 1. Add the IsDeleted property
entityType.GetOrAddProperty("IsDeleted", typeof(bool));
// 2. Create the query filter
var parameter = Expression.Parameter(entityType.ClrType);
// EF.Property<bool>(post, "IsDeleted")
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));
// EF.Property<bool>(post, "IsDeleted") == false
BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));
// post => EF.Property<bool>(post, "IsDeleted") == false
var lambda = Expression.Lambda(compareExpression, parameter);
builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}
// Many to Many relationship - HostApplicationUser
builder.Entity<HostApplicationUser>()
.HasKey(bc => new { bc.HostID, bc.Id });
builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.Host)
.WithMany(b => b.HostApplicationUsers)
.HasForeignKey(bc => bc.HostID);
builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.HostApplicationUsers)
.HasForeignKey(bc => bc.Id);
// Many to Many relationship - ClubApplicationUser
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });
builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);
builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);
// Many to Many relationship - ApplicationUserRole
builder.Entity<ApplicationUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });
userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
userRole.HasOne(ur => ur.User)
.WithMany(r => r.ApplicationUserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
// Added
var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
added.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
((IBaseEntity)entry.Entity).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Modified
var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
modified.ForEach(entry =>
{
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});
// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
}
}
Worked OK with standalone model/class ie. Club, Host, ApplicationUser ** BUT not bridging model/class ie. **ClubApplicationUser and HostApplicationUser
So we have to do manually on this (AssignClub.cshtml.cs for instance) by adding manually for creating/deleting as you can see below:
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);
... the deleted.Count() below returning 0 ... and the same with creating etc:
// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});
AssignClub.cshtml.cs -
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using AthlosifyWebArchery.Pages.Administrators.TournamentBatches;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using static AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
namespace AthlosifyWebArchery.Pages.Administrators.Users
{
//public class AssignClubUserModel : ClubNamePageModel
public class AssignClubUserModel : UserViewPageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor
)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public class AssignClubUserViewModel<ApplicationUser>
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid ClubID { get; set; }
public Guid OriginalClubID { get; set; }
public byte[] RowVersion { get; set; }
}
[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }
//public SelectList ClubNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();
AssignClubUser = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
Id = t.Id,
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
ClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
OriginalClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.ClubApplicationUsers.ElementAt(0).RowVersion
}).SingleAsync();
if (AssignClubUser == null)
return NotFound();
// Use strongly typed data rather than ViewData.
//ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
return Page();
// 1st approach:
// Modify the bridge model directly
/*var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
if (clubApplicationUserToUpdate == null)
return await HandleDeletedUser();
_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;
// This slightly tweek for this particular
// As the modified Change Track is not triggered
//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastDateModified").CurrentValue = DateTime.UtcNow;
//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastModifiedBy").CurrentValue = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (await TryUpdateModelAsync<ClubApplicationUser>(
clubApplicationUserToUpdate,
"AssignClubUser",
s => s.Id, s => s.ClubID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (ClubApplicationUser)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The club application user was deleted by another user.");
return Page();
}
var dbValues = (ClubApplicationUser)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
AssignClubUser.RowVersion = (byte[])dbValues.RowVersion;
ModelState.Remove("User.RowVersion");
}
}
*/
// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key
/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;
//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;
if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();clubApplicationUserDeleted
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/
//delete all club memberships and add new one
//var clubApplicationUserDeleted = await _context.ClubApplicationUser
// .FirstOrDefaultAsync(m => m.Id == id.ToString()
// && m.ClubID == AssignClubUser.ClubID && m.IsDeleted == );
var userID = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (AssignClubUser.ClubID != AssignClubUser.OriginalClubID)
{
var deletedClubApplicationUsers = _context.ClubApplicationUser.IgnoreQueryFilters()
.Where(post => post.Id == id.ToString()
&& post.ClubID == AssignClubUser.ClubID && EF.Property<bool>(post, "IsDeleted") == true);
if (deletedClubApplicationUsers.Count() > 0)
{
// Undo the deleted one
foreach (var deletedClubApplicationUser in deletedClubApplicationUsers)
{
var postEntry = _context.ChangeTracker.Entries<ClubApplicationUser>().First(entry => entry.Entity == deletedClubApplicationUser);
postEntry.Property("IsDeleted").CurrentValue = false;
postEntry.Property("LastDateModified").CurrentValue = DateTime.UtcNow;
postEntry.Property("LastModifiedBy").CurrentValue = userID;
postEntry.Property("DateDeleted").CurrentValue = null;
postEntry.Property("DeletedBy").CurrentValue = null;
}
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
else
{
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);
try
{
_context.Entry(clubApplicationUserToRemove).State = EntityState.Deleted;
await _context.SaveChangesAsync();
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
// Added the new one
var newClubApplicationUser = new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.ClubID,
DateCreated = DateTime.UtcNow,
CreatedBy = userID,
LastDateModified = DateTime.UtcNow,
LastModifiedBy = userID
};
_context.ClubApplicationUser.Add(newClubApplicationUser);
try
{
_context.Entry(newClubApplicationUser).State = EntityState.Added;
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
}
return RedirectToPage("./Index");
}
private async Task<IActionResult> HandleDeletedUser()
{
ClubApplicationUser deletedClubApplicationUser = new ClubApplicationUser();
ModelState.AddModelError(string.Empty,
"Unable to save. The club was deleted by another user.");
return Page();
}
private async Task setDbErrorMessage(ClubApplicationUser dbValues,
ClubApplicationUser clientValues, ApplicationDbContext context)
{
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}
}
}
ClubApplicationUser.cs model:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace AthlosifyWebArchery.Models
{
public class ClubApplicationUser
{
public Guid ClubID { get; set; }
public Club Club { get; set; }
public string Id { get; set; }
public ApplicationUser ApplicationUser { get; set; }
public DateTime DateCreated { get; set; }
public string CreatedBy { get; set; }
public DateTime LastDateModified { get; set; }
public string LastModifiedBy { get; set; }
public DateTime? DateDeleted { get; set; }
public string DeletedBy { get; set; }
public bool IsDeleted { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }
[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }
}
}
Any ideas? The current solution is working OK BUT it just has extra routines to add these DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy manually for this bridging model/class.
Environment:
.NET Core 2.2
SQL Server
Updates 1:
We even add this before the saving and it didn't trigger the automatic ChangeTracker.Entries():
_context.Entry(newClubApplicationUser).State = EntityState.Deleted;
await _context.SaveChangesAsync();
Updates 2:
Added the ClubApplicationUser model above.

automatically expand related entity with OData controller

I have these classes:
public class Items
{
[Key]
public Guid Id { get; set; }
public string ItemCode { get; set; }
public decimal SalesPriceExcl { get; set; }
public decimal SalesPriceIncl { get; set; }
public virtual ICollection<ItemPrice> SalesPrices { get; set; }
public Items()
{
SalesPrices = new HashSet<App4Sales_ItemPrice>();
}
}
public class ItemPrice
{
[Key, Column(Order = 0), ForeignKey("Items")]
public Guid Id { get; set; }
public virtual Items Items { get; set; }
[Key, Column(Order=1)]
public Guid PriceList { get; set; }
public decimal PriceExcl { get; set; }
public decimal PriceIncl { get; set; }
public decimal VatPercentage { get; set; }
}
I want to query the Items and automatically get the ItemPrice collection.
I've created an OData V3 controller:
// GET: odata/Items
//[Queryable]
public IQueryable<Items> GetItems(ODataQueryOptions opts)
{
SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);
Request.SetSelectExpandClause(expandOpts.SelectExpandClause);
return expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings()) as IQueryable<Items>;
}
But I get the error:
"Cannot serialize null feed"
Yes, some Items have no ItemPrice list.
Can I get past this error, or can I do something different?
Kind regards
Jeroen
I found the underlying error is:
Unable to cast object of type
'System.Data.Entity.Infrastructure.DbQuery1[System.Web.Http.OData.Query.Expressions.SelectExpandBinder+SelectAllAndExpand1[.Models.Items]]'
to type '.Models.Items'.
I've solved it after I came across this post: http://www.jauernig-it.de/intercepting-and-post-processing-odata-queries-on-the-server/
This is my controller now:
SelectExpandQueryOption expandOpts = new SelectExpandQueryOption(null, "SalesPrices", opts.Context);
Request.SetSelectExpandClause(expandOpts.SelectExpandClause);
var result = expandOpts.ApplyTo(db.Items.AsQueryable(), new ODataQuerySettings());
var resultList = new List<Items>();
foreach (var item in result)
{
if (item is Items)
{
resultList.Add((Items)item);
}
else if (item.GetType().Name == "SelectAllAndExpand`1")
{
var entityProperty = item.GetType().GetProperty("Instance");
resultList.Add((Items)entityProperty.GetValue(item));
}
}
return resultList.AsQueryable();
Jeroen
GetItems([FromODataUri] ODataQueryOptions queryOptions)
expanding on Jeroen's post. Anytime a select or expand is involved, OData wraps the results in a SelectAll or SelectSome object; so, we need to unwrap the values rather than do an direct cast.
public static class ODataQueryOptionsExtensions
{
public static IEnumerable<T> ApplyODataOptions<T>(this IQueryable<T> query, ODataQueryOptions options) where T : class, new()
{
if (options == null)
{
return query;
}
var queryable = options.ApplyTo(query);
if (queryable is IQueryable<T> queriableEntity)
{
return queriableEntity.AsEnumerable();
}
return UnwrapAll<T>(queryable).ToList();
}
public static IEnumerable<T> UnwrapAll<T>(this IQueryable queryable) where T : class, new()
{
foreach (var item in queryable)
{
yield return Unwrap<T>(item);
}
}
public static T Unwrap<T>(object item) where T : class, new()
{
var instanceProp = item.GetType().GetProperty("Instance");
var value = (T)instanceProp.GetValue(item);
if (value != null)
{
return value;
}
value = new T();
var containerProp = item.GetType().GetProperty("Container");
var container = containerProp.GetValue(item);
if (container == null)
{
return (T)null;
}
var containerType = container.GetType();
var containerItem = container;
var allNull = true;
for (var i = 0; containerItem != null; i++)
{
var containerItemType = containerItem.GetType();
var containerItemValue = containerItemType.GetProperty("Value").GetValue(containerItem);
if (containerItemValue == null)
{
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
continue;
}
var containerItemName = containerItemType.GetProperty("Name").GetValue(containerItem) as string;
var expandedProp = typeof(T).GetProperty(containerItemName);
if (expandedProp.SetMethod == null)
{
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
continue;
}
if (containerItemValue.GetType() != typeof(string) && containerItemValue is IEnumerable containerValues)
{
var listType = typeof(List<>).MakeGenericType(expandedProp.PropertyType.GenericTypeArguments[0]);
var expandedList = (IList)Activator.CreateInstance(listType);
foreach (var expandedItem in containerValues)
{
var expandedInstanceProp = expandedItem.GetType().GetProperty("Instance");
var expandedValue = expandedInstanceProp.GetValue(expandedItem);
expandedList.Add(expandedValue);
}
expandedProp.SetValue(value, expandedList);
allNull = false;
}
else
{
var expandedInstanceProp = containerItemValue.GetType().GetProperty("Instance");
if (expandedInstanceProp == null)
{
expandedProp.SetValue(value, containerItemValue);
allNull = false;
}
else
{
var expandedValue = expandedInstanceProp.GetValue(containerItemValue);
if (expandedValue != null)
{
expandedProp.SetValue(value, expandedValue);
allNull = false;
}
else
{
var t = containerItemValue.GetType().GenericTypeArguments[0];
var wrapInfo = typeof(ODataQueryOptionsExtensions).GetMethod(nameof(Unwrap));
var wrapT = wrapInfo.MakeGenericMethod(t);
expandedValue = wrapT.Invoke(null, new[] { containerItemValue });
if (expandedValue != null)
{
expandedProp.SetValue(value, expandedValue);
allNull = false;
}
}
}
}
containerItem = containerType.GetProperty($"Next{i}")?.GetValue(container);
}
if (allNull)
{
return (T)null;
}
return value;
}
}