How to know the user edited? - entity-framework-core

I am using Blazor server-side with ASP.NET Core 5 and EF Core 5. I would like that when a record is updated than the ModifiedBy and CreatedBy are generated automatically.
I have overriden SaveChangeAsync as follows:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ApplyInterfaces();
return base.SaveChangesAsync(cancellationToken);
}
private void ApplyInterfaces()
{
var userId = _httpContextAccessor?.HttpContext?.User?.Identity?.Name;
var currentUsername = !string.IsNullOrEmpty(userId)? userId: "Anonymous";
foreach (ICreatedBy entity in ChangeTracker.Entries().Where(x => x.Entity is ICreatedBy).Where(x => x.State == EntityState.Added).Select(x=> x.Entity))
{
entity.CreatedBy = currentUsername;
entity.CreatedDate = DateTime.Now;
}
foreach (IModifiedBy entity in ChangeTracker.Entries().Where(x=> x.State == EntityState.Modified).Where(x => x.Entity is IModifiedBy).Select(x => x.Entity))
{
entity.ModifiedBy = currentUsername;
entity.ModifiedDate = DateTime.Now;
}
}
_httpContextAccessor is injected to the DBContext. I added the services
services.AddHttpContextAccessor();
Everything works fine on IIS Express. But when I publish to IIS _httpContextAccessor.HttpContext is null.
Where did I miss something?

You should not use IHttpContextAccessor in Blazor apps (source).
It's not clear from your code sample if this code is in a Blazor component/page or in a database access library. If it's in the database layer you should modify SaveChangesAsync to add a string username parameter, which you can then pass to ApplyInterfaces.
Getting the user should be the responsibility of the Blazor page/component, using either AuthenticationStateProvider or an AuthorizeView component.
#page "/example"
#inject AuthenticationStateProvider auth;
<button #OnClick="Save">Save</button>
#code
{
async Task Save()
{
var state = await auth.GetAuthenticationStateAsync();
var username = state.User.Identity?.Name ?? "[anon]";
await someObject.SaveChangesAsync(username);
}
}

Related

EF Core 5.x + OData 7.5.6 - $Top doesn't insert TOP inside of EF Query

I'm thinking I missed something very simply but here is what I'm trying to todo. I have a .NET Core 5 project with EF Core 5 + OData 7.5.6. Everything appears to be working except for INSERT a TOP command in the generated SQL Query. Here is my controller. Very simple.
[EnableQuery]
[ApiController]
[Route("odata/[controller]")]
public class ConferenceHistoryController : ControllerBase
{
private readonly cdr_database_2Context _db;
public ConferenceHistoryController(cdr_database_2Context db)
{
_db = db;
}
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query.ToList();
}
}
When I send in my request to:
https://localhost:44355/odata/ConferenceHistory/?$select=RecordId,version&$top=5
The resulting SQL query is:
SELECT [0].[endDateTime], [0].[id], [0].[organizer], [0].[ParticipantCount], [0].[participants], [0].[PoorCall], [0].[type], [0].[version]
FROM [000701_CallDataRecord] AS [0]
As you can see, it's missing both the TOP and the SELECT commands. I have the following in my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add CORS to Project
services.AddCors(options =>
{
options.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
// Add OData
services.AddOData();
services.AddControllers();
services.AddDbContext<cdr_database_2Context>(options =>
options.UseSqlServer(Configuration.GetConnectionString("CDRConnection"))
);
// Add Swagger Support
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ODataAPI", Version = "v1" });
});
SetOutputFormatters(services);
}
And
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Use HTTPS & Routing
app.UseHttpsRedirection();
app.UseRouting();
// Auth???
app.UseAuthorization();
// Swagger Configure
app.UseSwagger();
app.UseSwaggerUI((config) =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "Swagger Odata Demo Api");
});
// Setup Endpoint for EDM
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.EnableDependencyInjection();
endpoints.Select().Expand().Filter().OrderBy().MaxTop(50).Count();
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
}
And
private IEdmModel GetEdmModel()
{
// Add OData - EDM Definitions Below
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<WeatherForecast>("WeatherForecast");
odataBuilder.EntitySet<_000701CallDataRecord>("ConferenceHistory");
return odataBuilder.GetEdmModel();
}
Just looking for some direction on where I could have gone wrong. Everything else seems to be working really well.
Your controller needs to derive from ODataController not ControllerBase
You need to decorate GetConferenceList() with [Queryable]
OK, I figured out where the issue was in my above. It was with using the .ToList and the IEumerable. So if I changed this:
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query.ToList();
}
To:
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query;
}
It works. It's also worth noting that the .ToQueryString() doesn't show the OData/EF insertion of the TOP command. But if I enable .EnableSensitiveDataLogging() in the Startup.cs file, then I clearly see it being inserted into it.
Apparently, calling .ToList() will cause it to process and ignore any of the fancy OData commands. That was the only change and all was good then.

How do I use Audit.NET Entity Framework Data Provider to save Audit.NET WebAPI audit logs?

I am having difficulty understanding the documentation for the Audit.NET Entity Framework Data Provider, to save Audit.NET WebAPI audit logs to my database.
This is how I have my Audit configuration set, just to test. I have a breakpoint inside the AuditEntityAction on entity.ChangeType = ev.EventType, but this never gets hit when I call an audited action on my controller.
Audit.Core.Configuration.Setup()
.UseEntityFramework(x =>
x.AuditTypeMapper(t => typeof(AuditLog))
.AuditEntityAction<AuditLog>((ev, entry, entity) =>
{
entity.ChangeType = ev.EventType;
entity.ObjectType = entry.EntityType.Name;
entity.PrimaryKey = "test";
entity.TableName = "test";
entity.UserId = entry.CustomFields[UserIdField].ToString();
})
.IgnoreMatchedProperties()
);
On my controller action, I have the decorator:
[AuditApi(EventTypeName = "Organisation:Create", IncludeRequestBody = true, IncludeResponseBody = true)]
Is this correct? I am not very clear on this, and I would appreciate some pointers.
The Entity Framework Data Provider is part of the library Audit.EntityFramework and was designed to exclusively store the audits that are generated by an audited Entity Framework DbContext.
So it will not work for WebApi events of any other kind of event.
Here you can see how the audit event is discarded if it's not an AuditEventEntityFramework
So you should create your own Custom Data Provider or maybe use the SQL Data Provider.
You can use Audit.NetWebApi package to get the WebApiAudit logs
public static void UseAudit(this IApplicationBuilder app, IHttpContextAccessor contextAccessor)
{
Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
var entityTrack = scope.Event.GetEntityFrameworkEvent();
var requestTrack = scope.Event.GetWebApiAuditAction();
if (entityTrack!=null)
{
foreach (var item in entityTrack.Entries)
{
scope.Event.CustomFields[Table] = item.Table;
scope.Event.CustomFields[Action] = item.Action;
}
}
else if(requestTrack!=null)
{
scope.Event.CustomFields[Action] = $"{requestTrack.ActionName}:{requestTrack.ActionName}";
scope.Event.CustomFields[RequestBody] = requestTrack.RequestBody.Value.ToString();
scope.Event.CustomFields[ResponseBody] = requestTrack.ResponseBody?.Value?.ToString()?? string.Empty;
scope.Event.CustomFields[Exception] = requestTrack.Exception?? string.Empty;
}
});
}
And then put this function in Startup.cs ConfigureApp
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IHttpContextAccessor contextAccessor)
{
app.UseAudit(contextAccessor);
}
Constants used:
private const string Table = "Table";
private const string Action = "Action";
private const string RequestBody = "RequestBody";
private const string ResponseBody = "ResponseBody";
private const string Exception = "Exception";

Cannot Create A DbSet for ApplicationUser

I have an asp.net core project that I am refactoring. Previously I had all my database logic contained within the project, however as we are now adding a WebAPI, I have moved the database logic to a separate .net core standard project, so it is shared between the two projects.
This seems to work fine in the new web api, however I am having issues in the original project, relating to signInManager and the ApplicationUser class.
All compiles just fine, however, I get the following error during runtime:
InvalidOperationException: Cannot create a DbSet for 'ApplicationUser' because this type is not included in the model for the context.`
I have also moved this ApplicationUser class to the new DAL project, and as far as I can see, i've updated all references to it (certainly enough to pass the compile time checks).
My startup.cs is as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// password policy settings
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
// Add session cookie
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults
// the path to /Account/Login.
options.LoginPath = "/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults
// the path to /Account/AccessDenied.
options.AccessDeniedPath = "/Account/AccessDenied";
options.SlidingExpiration = true;
// Disable redirect to login page for unauthorized requests to / api resources
options.Events.OnRedirectToLogin = context =>
{
if (context.Request.Path.StartsWithSegments("/api") && context.Response.StatusCode == StatusCodes.Status200OK)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.FromResult<object>(null);
}
else
{
context.Response.Redirect(context.RedirectUri);
return Task.FromResult<object>(null);
}
};
});
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
// add our Db handlers
services.AddScoped<DAL.Services.Interfaces.IServices, DAL.Services.Services>();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider services)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var cultureInfo = new CultureInfo("en-GB");
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
I have a "using" statement at the top as follows:
using IceBowl.DAL;
using IceBowl.DAL.Models;
So the call to "AddIdentity" is passing in the right ApplicationUser - in fact, there is only one ApplicationUser class, I deleted the original.
The code seems to be having issues on the following line:
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
There's something it doesnt like about sign in manager, but I'm at a loss to explain what. All references have been updated and now point to the DAL that contains the datacontext and ApplicationUser class.
Any pointers?

Entity Framework 6 dbContext not saving changes

In our MVC 5 project our database context is instantiate in the AccountController like this
private CustomersContext _customersContext;
public CustomersContext CustContext
{
get
{
return _customersContext ?? new CustomersContext();
}
private set
{
_customersContext = value;
}
}
Each customer is referred by a number of sources. The routine below changes the UserId of the referral source to a new user.
var referralList = CustContext.Referrals.Where(d => d.UserId == membershipUser.Id);
foreach (Referral referral in referralList)
{
referral.UserId = newUser.Id;
}
Stepping trough the code I can see referral.UserId being updated. However
var result = await CustContext.SaveChangesAsync();
returns 0. The database is not updated.
CustomersContext looks like this
{
public partial class CustomersContext : IdentityDbContext<ApplicationUser>//, ICustomersContext
{
public CustomersContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static CustomersContext Create()
{
return new CustomersContext();
}
public virtual DbSet<ReferralSource> ReferralSources { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>()
.HasMany(e => e.Referrals)
.WithRequired(e => e.User)
.HasForeignKey(e => e.UserId)
.WillCascadeOnDelete(false);
I don't see any sql emitted in SQL Profiler. Why doesn't the database context save changes?
Before calling var result = await CustContext.SaveChangesAsync(); you need to set the state of the entities that you want to be modified. Somthing like:
var referralList = CustContext.Referrals.Where(d => d.UserId == membershipUser.Id);
foreach (Referral referral in referralList)
{
referral.UserId = newUser.Id;
CustContext.Entry(referral).State = System.Data.Entity.EntityState.Modified;
}
var result = await CustContext.SaveChangesAsync();
The answer provided by #Issac did not solve my problem, but it did put me on the road to a solution. The error
An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
suggested there were multiple instances of dbContext. I removed the context from the CTOR and instantiated the context within a using statement
using (CustomersContext customersContext = new CustomersContext())
{
var referralList = customersContext.Referrals.Where(d => d.UserId == membershipUser.Id);
foreach (Referral referral in referralList)
{
referral.UserId = newUser.Id;
}
var result = await customersContext.SaveChangesAsync();
}
and now all is tickety-boo

How to access repository from IRouter

I'm developing modular application and I'd like for entities from different modules to be able to register their own friendly url slugs.
app.UseMvc(routes =>
{
routes.Routes.Add(new SlugRouter(routes.DefaultHandler));
(...)
});
But following code throws Cannot access a disposed object. Object name: 'CommerceDbContext'. when trying to access slug from the repository.
public class SlugRouter : IRouter
{
private readonly IRouter _target;
public SlugRouter(IRouter target)
{
_target = target;
}
public async Task RouteAsync(RouteContext context)
{
var slugRepository = context.HttpContext.RequestServices.GetService<IRepository<SlugEntity>>();
// ERROR: Cannot access a disposed object. Object name: 'CommerceDbContext'
var urlSlug = await slugRepository.GetAllIncluding(x => x.EntityType).FirstOrDefaultAsync(x => x.Slug == context.HttpContext.Request.Path.Value);
(...)
}
It must be something simple I'm missing to be able to access the repository from router. Thanks for any help.
Begin a unit of work:
public async Task RouteAsync(RouteContext context)
{
var slugRepository = context.HttpContext.RequestServices.GetService<IRepository<SlugEntity>>();
var unitOfWorkManager = context.HttpContext.RequestServices.GetService<IUnitOfWorkManager>();
using (var uow = unitOfWorkManager.Begin())
{
var urlSlug = await slugRepository.GetAllIncluding(x => x.EntityType).FirstOrDefaultAsync(x => x.Slug == context.HttpContext.Request.Path.Value);
await uow.CompleteAsync();
}
}
Access IModel. You do not need dbContext for.
for entities from different modules to be able to register their own
friendly url slugs
I do it this way:
1) move OnModelCreating to static methiod
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
BuildModel(modelBuilder);
}
public static void BuildModel(ModelBuilder modelBuilder)
{
// ...
}
2) Create model where you need:
var conventionSet = new ConventionSet();
var modelBuilder = new ModelBuilder(conventionSet);
AdminkaDbContext.BuildModel(modelBuilder);
var mutableModel = modelBuilder.Model;
There is your meta (in mutableModel ). You can loop through entities (types of entities).