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

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";

Related

How to know the user edited?

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);
}
}

.Net 5 change DbContext in controller

I have a design where I have one "master" database and multiple "client" databases. When I get a request I lookup in the master database and setup the connection to the right client database.
I'm now trying to design the same in .net 5, where I setup the masterDB in StartUps ConfigureServices():
services.AddDbContext<Models.DataContext.MasterContext>(options =>
options.UseSqlServer("Name=MasterDB"));
I then on the request lookup in the MasterDB as the first thing in every controllers methods and find the connectionString for the clientDB.
But how do I then set it up at that point in time? While also not having to think about disposal of the connection, like when it's passed in using dependency injection, it's handled.
Any advice to do things slightly different are also encouraged.
Inject your MasterContext into a service that provides connection string lookups for your "client" databases (probably with caching). Then use that when resolving and configuring your "client" DbContext.
Something like this:
class ClientDatabaseService
{
MasterDbContext db;
IHttpContextAccessor context;
static Dictionary<string, string> cache = null;
public ClientDatabaseService(MasterDbContext db, IHttpContextAccessor context)
{
this.db = db;
this.context = context;
if (cache == null) RefreshCache();
}
public void RefreshCache()
{
cache = db.Clients.Select(c => new { c.ClientID, c.ConnectionString }).ToDictionary(c => c.ClientID, c => c.ConnectionString);
}
public string GetClientConnectionString()
{
var clientId = context.HttpContext.User.FindFirst("ClientID").Value;
return cache[clientId];
}
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<MasterDbContext>();
services.AddHttpContextAccessor();
services.AddScoped<ClientDatabaseService>();
services.AddDbContext<ClientDbContext>((services, options) =>
{
var constrService = services.GetRequiredService<ClientDatabaseService>();
var constr = constrService.GetClientConnectionString();
options.UseSqlServer(constr, o => o.UseRelationalNulls());
});
}

Return a custom response when using the Authorize Attribute on a controller

I have just implemented the Bearer token and I have added the Authorize attribute to my controller class and that works fine. It looks like this:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
What I would like to do is to create a more complex response from the server when it fails, rather then the standard 401.
I tried filters but they are not invoked at all.
Any ideas how to do this?
Have a custom scheme, custom authorization handler and poof!
Notice that I injected the Handler in ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationOptions.DefaultScheme, o => { });
ApiKeyAuthenticationOptions
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
public const string HeaderKey = "X-Api-Key";
}
ApiKeyAuthenticationHandler
/// <summary>
/// An Auth handler to handle authentication for a .NET Core project via Api keys.
///
/// This helps to resolve dependency issues when utilises a non-conventional method.
/// https://stackoverflow.com/questions/47324129/no-authenticationscheme-was-specified-and-there-was-no-defaultchallengescheme-f
/// </summary>
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private readonly IServiceProvider _serviceProvider;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider)
: base (options, logger, encoder, clock)
{
_serviceProvider = serviceProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Request.Headers[ApiKeyAuthenticationOptions.HeaderKey];
if (string.IsNullOrEmpty(token)) {
return Task.FromResult (AuthenticateResult.Fail ("Token is null"));
}
var customRedisEvent = _serviceProvider.GetRequiredService<ICustomRedisEvent>();
var isValidToken = customRedisEvent.Exists(token, RedisDatabases.ApiKeyUser);
if (!isValidToken) {
return Task.FromResult (AuthenticateResult.Fail ($"Invalid token {token}."));
}
var claims = new [] { new Claim ("token", token) };
var identity = new ClaimsIdentity (claims, nameof (ApiKeyAuthenticationHandler));
var ticket = new AuthenticationTicket (new ClaimsPrincipal (identity), Scheme.Name);
return Task.FromResult (AuthenticateResult.Success (ticket));
}
}
Focus on the handler class. Apart from the sample code I've provided, simply utilise the base class properties like Response to set your custom http status code or whatever you may need!
Here's the derived class if you need it.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1?view=aspnetcore-3.1

Viewmodel not updating model after add or insert to list

I'm fairly new to MVVM and Entity framework and I've hit a problem trying to add new records to my SQL database through MVVM. Below is the first and last part of my viewmodel which loads from my entity framework and this is working fine.
internal class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
private TEAMSEntities ctx = new TEAMSEntities();
public MainWindowViewModel()
{
FillSales();
}
public void AddASale()
{
Tbl_Sales newsale = new Tbl_Sales();
newsale.SALEID = "2018...";
newsale.SALE = "....";
newsale.SALEDESC = "....";
newsale.START = Convert.ToDateTime("12/04/2018");
newsale.SESSIONS = 2;
newsale.DAYS = 2;
newsale.LOTS = 100;
newsale.FIRSTLOT = 1;
newsale.LASTLOT = 100;
Sales.Add(newsale);
SaveChanges();
}
#region Sale
private void FillSales()
{
var q = (from a in ctx.Tbl_Sales
select a).ToList();
this.Sales = q;
}
private List<Tbl_Sales> _sales;
public List<Tbl_Sales> Sales
{
get
{
return _sales;
}
set
{
_sales = value;
NotifyPropertyChanged();
}
}
private Tbl_Sales _selectedSale;
public Tbl_Sales SelectedSale
{
get
{
return _selectedSale;
}
set
{
_selectedSale = value;
NotifyPropertyChanged();
}
}
#endregion Sale
.
.
.
.
public void SaveChanges()
{
ctx.SaveChanges();
}
private void NotifyPropertyChanged([CallerMemberName] String
propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Dispose()
{
((IDisposable)ctx).Dispose();
}
}
When I make changes to existing data in the view bound to the viewmodel and call the SaveChanges method in the viewmodel it saves the change back to the SQL database every time. If I call the AddASale method it adds that sale to the list but doesn't refresh the UI control bound to Sales and doesn't pass the newly created sale back to the SQL DB. Through debugging I can see the set being called in the Sales property when the LINQ code runs but it doesn't fire when I add a new sale through the AddASale code which is probably why the UI isn't updating...?
Can anyone offer any guidance as to what I'm doing wrong?
Thanks in advance.
Alex
First, add your new entity in your context and save it like this :
Tbl_Sales newsale = new Tbl_Sales
{
SALEID = "2018...",
SALE = "....",
SALEDESC = "....",
START = Convert.ToDateTime("12/04/2018"),
SESSIONS = 2,
DAYS = 2,
LOTS = 100,
FIRSTLOT = 1,
LASTLOT = 100
};
ctx.Tbl_Sales.Add(newsale);
SaveChanges();
after that, I think you have to refresh your list manually :
FillSales();
Here, a link to MSDN
Hope I helped you

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).