How to create multiple POST methods on an Azure Mobile Service? - c#

I have an Azure Mobile Service with multiple controllers. One of my controllers (TestSetController) has some extra methods to check on insert.
Problem I need to solve: The TestSet table has two different types of TestSets, one for a local team and another for a field team. The table contains data for both and the records are differentiated by a "TeamType" field which says if the local team inserted the TestSet or the field team did. On any insert I want to check if a similar TestSet exists that was inserted by the other team. I want to compare the TestSets (if found) then do some other inserts/updates on the same table if the TestSets are different.
However, I keep getting this error:
Exception=System.InvalidOperationException: Multiple actions were found that match the request:
PostTestSetDTO on type sbp_ctService.Controllers.TestSetController
CheckForDiscrepancy on type sbp_ctService.Controllers.TestSetController
CompareTestPointAttempts on type sbp_ctService.Controllers.TestSetController
at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext)
at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext)
at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext(), Id=f07761ae-1be7-4f00-90b0-685dd0c108f3, Category='App.Request'
Here's my controller:
public class TestSetController : TableController<TestSetDTO>
{
private Context context;
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
context = new Context();
DomainManager = new SimpleMappedEntityDomainManager<TestSetDTO, TestSet>(context, Request, Services, testset => testset.Id);
}
// GET tables/TestSet
[QueryableExpand("TestPointAttempts")]
public IQueryable<TestSetDTO> GetAllTestSetDTO()
{
return Query();
}
// GET tables/TestSet/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<TestSetDTO> GetTestSetDTO(string id)
{
return Lookup(id);
}
// PATCH tables/TestSet/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<TestSetDTO> PatchTestSetDTO(string id, Delta<TestSetDTO> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/TestSet/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostTestSetDTO(TestSetDTO item)
{
TestSet testSet = AutoMapper.Mapper.Map<TestSetDTO, TestSet>(item);
this.CheckForDiscrepancy(testSet);
TestSetDTO current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/TestSet/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteTestSetDTO(string id)
{
return DeleteAsync(id);
}
public TestSet CheckForDiscrepancy(TestSet sourceTestSet)
{
// Set the team type to search for opposite to the one being posted.
string searchTeamType = null;
if (sourceTestSet.TestTeamType == "D")
{
searchTeamType = "F";
}
if (sourceTestSet.TestTeamType == "F")
{
searchTeamType = "D";
}
var testSetTable = context.TestSets;
TestSet foundTestSet = (from ts in testSetTable
where ts.TileId == sourceTestSet.TileId && ts.ScenarioId == sourceTestSet.ScenarioId && ts.TestTeamType.StartsWith(searchTeamType)
select ts).SingleOrDefault();
// If no other test set was found from the opposing team then the test set is missing.
// Else a testSet was found so continue with checks.
if (foundTestSet == null)
{
sourceTestSet.DiscrepancyTypeId = DiscrepancyType.Missing.ToString();
}
else
{
var testPointAttemptTable = context.TestPointAttempts;
// Get all of the associated TestPointAttempts for each testSet.
sourceTestSet.TestPointAttempts = (from tpa in testPointAttemptTable
where tpa.TestSetId == sourceTestSet.Id
orderby tpa.TestAttemptNumber
select tpa).ToList<TestPointAttempt>();
foundTestSet.TestPointAttempts = (from tpa in testPointAttemptTable
where tpa.TestSetId == foundTestSet.Id
orderby tpa.TestAttemptNumber
select tpa).ToList<TestPointAttempt>();
bool matchingTestSets = CompareTestPointAttempts(sourceTestSet.TestPointAttempts, foundTestSet.TestPointAttempts);
if (!matchingTestSets)
{
sourceTestSet.DiscrepancyTypeId = DiscrepancyType.Discrepancy.ToString();
sourceTestSet.DiscrepancyTestSetId = foundTestSet.Id;
}
}
return sourceTestSet;
}
public bool CompareTestPointAttempts(IEnumerable<TestPointAttempt> sourceTPAs, IEnumerable<TestPointAttempt> foundTPAs)
{
bool pass = false;
// First check if the total number of testPointAttempts are the same
if (sourceTPAs.Count() == foundTPAs.Count())
{
foreach (TestPointAttempt sTpa in sourceTPAs)
{
bool foundMatch = false;
foreach (TestPointAttempt fTpa in foundTPAs)
{
if (sTpa.TestAttemptNumber == fTpa.TestAttemptNumber)
{
if (sTpa.TalkIn == fTpa.TalkIn && sTpa.TalkOut == fTpa.TalkOut)
{
foundMatch = true;
}
}
}
if (!foundMatch)
{
return pass;
}
}
// The foreach loop finished successfully meaning all matches were found
pass = true;
}
return pass;
}
/// <summary>
/// The type of discrepancy a TestSet can have.
/// </summary>
public enum DiscrepancyType
{
Discrepancy,
Missing,
None
}
}
}
I'm using Data Transfer Objects (DTOs) to map between the entity models. Any help would be appreciated. I've looked at some different answers on StackOverflow for ASP.NET but they all talk about updating the config.Routes. This is for an Azure Mobile Service which might have different requirements than a simple ASP.NET website though.

It was as simple as making the two methods private and leaving the actual POST method as public. ASP.NET will only make routes automatically for public methods.

Related

How to implement IConvertible

I have some test code to check if 2 sides are equal.
public void GetCompanies_WithCorrectCompanyRequest_ReturnCompanyDtos()
{
// Arrange
var companyRequset = new CompanyRequest();
// Act
var companyDtos = _datlinqServiceMock.GetCompanies(companyRequset);
// Assert
Assert.IsTrue(companyDtos != null && companyDtos.Any());
Assert.AreEqual(DatlinqServiceMock.CompanyName, companyDtos.FirstOrDefault().Name);
}
That calls this.
public class DatlinqServiceMock: DatlinqService
{
public static string CompanyName = "Company_Test";
public override T GetApi<Q,T>(string apiMethod, Q request)
{
var companyList = new List<Company>()
{
new Company(){ Name = CompanyName}
};
return (T)Convert.ChangeType(companyList, typeof(T));
}
}
GetCompanies:
public List<Company> GetCompanies(CompanyRequest request)
{
if (request == null)
{
return new List<Company>();
}
var searchCompany = new SearchCompanyRequest();
searchCompany.Query = request.Name;
searchCompany.DatlinqKey = ConfigurationManager.AppSettings["Datlinq_Key"];
var searchCompanyResponse = GetApi<SearchCompanyRequest,SearchCompanyResponse>(DatlinqApiMethod.SearchCompany, searchCompany);
var companies = searchCompanyResponse.Result
.Select(c => new Company { Id = c.Id, Name = c.Name })
.ToList();
return companies;
}
GetApi:
public virtual T GetApi<Q,T>(string apiMethod, Q request)
{
var result = default(T);
try
{
var url = String.Format("{0}{1}", _apiUrl, apiMethod);
if (request != null)
{
url = QueryHelpers.AddQueryString(url, request.ToDictionary());
}
var apiResponse = _httpClient.GetAsync(url).Result;
if (apiResponse.IsSuccessStatusCode)
{
string apiResponseString = apiResponse.Content.ReadAsStringAsync().Result;
if (!string.IsNullOrEmpty(apiResponseString))
{
result = JsonConvert.DeserializeObject<T>(apiResponseString);
}
}
}
catch (Exception)
{
// do something
}
return result;
}
And I get an error when I execute the first test
Message: 
Test method Lavazza.ContractTool.Datlinq.Tests.Services.DatlinqServiceTests.GetCompanies_WithCorrectCompanyRequest_ReturnCompanyDtos threw exception:
System.InvalidCastException: Object must implement IConvertible.
Stack Trace: 
Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
Convert.ChangeType(Object value, Type conversionType)
DatlinqServiceMock.GetApi[Q,T](String apiMethod, Q request) line 79
DatlinqService.GetCompanies(CompanyRequest request) line 23
DatlinqServiceTests.GetCompanies_WithCorrectCompanyRequest_ReturnCompanyDtos() line 32
I hope this is enough code to know what the problem/solution is if not let me know what you need.
To answer some question asked below.
Dai: I am trying to find what this is and why I need it because it came up in the error but the microsoft docs don't make it clear for me.
Jeroen: It is kind of a legacy project so I can't/won't add extra dependencies.
The "issue" here is that your GetApi method is designed to return a sort of generic response - in this specific case SearchCompanyResponse - but you're trying to override it to return a List<Company> which is wrong because that is what your GetCompanies method does (ie turn a SearchCompanyResponse to a List<Company>).
Suffice it to say this has nothing to do with implementing IConvertable.
What you would be better off doing is serializing a SearchCommpanyResponse that you wish to use for testing to a file, and using that file-based response for your testing.
That way your overridden "Mock" would be
public override T GetApi<Q,T>(string apiMethod, Q request)
{
return JsonConvert.DeserializeObject<T>(File.ReadAllText("mytestfile.json"));
}

Fluent Validation changing CustomAsync to MustAsync

Could some one please help me to resolved this? i'm trying to change CustomAsync to MustAsync, but i couldn't make things to work. Below is my custom method
RuleFor(o => o).MustAsync(o => {
return CheckIdNumberAlreadyExist(o)
});
private static async Task<ValidationFailure> CheckIdNumberAlreadyExist(SaveProxyCommand command)
{
if (command.Id > 0)
return null;
using (IDbConnection connection = new SqlConnection(ConnectionSettings.LicensingConnectionString))
{
var param = new DynamicParameters();
param.Add("#idnumber", command.IdNumber);
var vehicle = await connection.QueryFirstOrDefaultAsync<dynamic>("new_checkDuplicateProxyIdNumber", param, commandType: CommandType.StoredProcedure);
return vehicle != null
? new ValidationFailure("IdNumber", "Id Number Already Exist")
: null;
}
}
To make it work with the latest version of the FluentValidation, I had to use the codes like below.
RuleFor(ws => ws).MustAsync((x, cancellation) => UserHasAccess(x)).WithMessage("User doesn't have access to perform this action");
Please notice the lambda expression here MustAsync((x, cancellation) => UserHasAccess(x)), without this I was always getting an error as cannot convert from 'method group' to 'Func<Worksheet, CancellationToken, Task<bool>>
Below is my custom UserHasAccess function.
private async Task <bool> UserHasAccess(Worksheet worksheet) {
var permissionObject = await _dataProviderService.GetItemAsync(worksheet.FileItemId);
if (permissionObject is null) return false;
if (EditAccess(permissionObject.Permission)) return true;
return false;
}
I'm assuming you're using a version of FluentValidation prior to version 6, as you're not passing in a Continuation Token, so I've based my answer on version 5.6.2.
Your example code does not compile, for starters, as you're missing a semi-colon in your actual rule. You are also evaluating two different properties on the SaveProxyCommand parameter.
I've built a very small POC based on some assumptions:
Given 2 classes:
public class SaveProxyCommand {
public int Id { get; set; }
}
public class ValidationFailure {
public string PropertyName { get; }
public string Message { get; }
public ValidationFailure(string propertyName, string message){
Message = message;
PropertyName = propertyName;
}
}
And a validator:
public class SaveProxyCommandValidator : AbstractValidator<SaveProxyCommand>{
public SaveProxyCommandValidator()
{
RuleFor(o => o).MustAsync(CheckIdNumberAlreadyExists)
.WithName("Id")
.WithState(o => new ValidationFailure(nameof(o.IdNumber), "Id Number Already Exist"));
}
private static async Task<bool> CheckIdNumberAlreadyExists(SaveProxyCommand command) {
if (command.Id > 0)
return true;
var existingIdNumbers = new[] {
1, 2, 3, 4
};
// This is a fudge, but you'd make your db call here
var isNewNumber = !(await Task.FromResult(existingIdNumbers.Contains(command.IdNumber)));
return isNewNumber;
}
}
I didn't include the call to the database, as that's not part of your problem. There are a couple of things of note here:
You're not setting the .WithName annotation method, but when you're setting up a validation rule for an object you have to do this, as FluentValidation expects you to specify specific properties to be validated by default, if you pass in an entire object it just doesn't know how to report errors back.
Must/MustAsync need to return a bool/Task<bool> instead of a custom object. To get around this, you can specify a custom state to be returned when failing validation.
You can then get access to this like this:
var sut = new SaveProxyCommand { Id = 0, IdNumber = 3 };
var validator = new SaveProxyCommandValidator();
var result = validator.ValidateAsync(sut).GetAwaiter().GetResult();
var ValidationFailures = result.Errors?.Select(s => s.CustomState).Cast<ValidationFailure>();
The above does not take into account empty collections, it's just an example of how to dig into the object graph to retrieve custom state.
As a suggestion, fluentvalidation works best if you set up individual rules per property, instead of validating the entire object. My take on this would be something like this:
public class SaveProxyCommandValidator : AbstractValidator<SaveProxyCommand>{
public SaveProxyCommandValidator()
{
RuleFor(o => o.IdNumber).MustAsync(CheckIdNumberAlreadyExists)
.Unless(o => o.Id > 0)
.WithState(o => new ValidationFailure(nameof(o.IdNumber), "Id Number Already Exist"));
}
private static async Task<bool> CheckIdNumberAlreadyExists(int numberToEvaluate) {
var existingIdNumbers = new[] {
1, 2, 3, 4
};
// This is a fudge, but you'd make your db call here
var isNewNumber = !(await Task.FromResult(existingIdNumbers.Contains(numberToEvaluate)));
return isNewNumber;
}
}
This read more like a narrative, it uses the .Unless construct to only run the rule if Id is not more than 0, and does not require the evaluation of the entire object.

Restful Web Api parameter as an array of int

Ideally I would like to have an URL in following format:
/api/categories/1,2,3...N/products
And this would return all products for the specified categories. Having one API call with multiple category IDs saves me several database calls, thus improves performance.
I can easily implement this in a following way.
public HttpResponseMessage GetProducts(string categoryIdsCsv)
{
// <1> Split and parse categoryIdsCsv
// <2> Get products
}
However, this doesn't look like a clean clean solution, and possibly breaking SRP principle. I also tried using ModelBinder, however it adds parameters to query string.
Questions:
Is there a clean way to implement such URL structure?
Or is there a different/better approach to retrieve all products for multiple categories?
Please let me know if you need any further clarification.
I've just found an answer to my question. Route attribute had missing parameter when using ModelBinder.
[Route("api/categories/{categoryIds}/products")]
public HttpResponseMessage GetProducts([ModelBinder(typeof(CategoryIdsModelBinder))] CategoryIds categoryIds)
{
// <2> Get products using categoryIds.Ids
}
And CategoryIds would be
public class CategoryIds
{
public List<int> Ids{ get; set; }
}
And CategoryIdsModelBinder would be
public class CategoryIdsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(CategoryIds))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
return false;
}
var values = val.AttemptedValue.Split(',');
var ids = new List<int>();
foreach (var value in values)
{
int intValue;
int.TryParse(value.Trim(), out intValue);
if (intValue > 0)
{
ids.Add(intValue);
}
}
if (ids.Count > 0)
{
var result = new CategoryIds
{
Ids= ids
};
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
We can use Post methods
[RoutePrefix ( "api/categories" )]
public class TestController
{
[HttpPost]
[Route ( "getProducts" )]
public HttpResponseMessage GetProducts ( HttpRequestMessage request )
{
HttpResponseMessage message = null;
string input = string.Empty;
input = request.Content.ReadAsStringAsync ().Result;
var ids = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>> ( input );
}
}
Unfortunately Web API can not parse your data as array or as some kind of your custom object out of the box.
If you want to parse your url param as array you can try to do:
Write your own route constraint which will read and convert your param from string to array of ints/strings/whatever;
Write your custom type converter and use it with your data model;
write your value provider and also use it with your data model
Use parameter binding
Moreover you can always use query params which is never will break principles of REST :)
Please see more details about here and here
Hope that helps

How to make Delete and Edit methods in MVC without DB

I am building a simple MVC CRUD without using a database, but just making methods in a Repository model class.
To make it easier to understand i have 2 model classes. MyNote in which i have some properties and NoteRepository in which i have a list with the properties.
Then I've made a NoteController and i have already made Get and Create methods, but i can't seem to figure out what to write to make an Edit and Delete method? Hope you guys can help.
Here you will see some of the code from my project:
[HttpPost]
public ActionResult Create(MyNote mn)
{
try
{
note.Create(mn);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
this is the create from the Controller.
public static List<MyNote> notes = new List<MyNote>();
public NoteRepository()
{
notes.Add(new MyNote() { ID = 1, Titel = "Morgenmad", OprettelsesDato = DateTime.Now, Note = "Spis morgenmad i dag" });
notes.Add(new MyNote() { ID = 2, Titel = "Frokost", OprettelsesDato = DateTime.Now, Note = "Spis frokost i dag" });
notes.Add(new MyNote() { ID = 3, Titel = "Aftensmad", OprettelsesDato = DateTime.Now, Note = "Spis aftensmad i dag" });
}
public void Create(MyNote mn)
{
notes.Add(mn);
}
here is the repository class with the list and the method for the create method.
and please, ask if i have missed something! Thank you :-)
It looks like you're using a List for your in-memory repository. For delete, you can implement something like this:
public bool Delete (MyNote noteToDelete) {
return notes.Remove(noteToDelete);
}
Edit: However, in this case, the list will check for reference equality. Since you have an ID, which I will assume is unique, you can instead do this:
public bool Delete(MyNote noteToDelete) {
var matchingNote = notes.FirstOrDefault(n => n.ID == noteToDelete.ID);
return notes.Remove(matchingNote);
}
You could also implement IEquatable on your MyNote class to change how your notes are compared with each other, and return a valid match when the IDs are the same.
For the IEquatable example, you would want to change the class definition for MyNote to look like:
public class MyNote : IEquatable<MyNote>
and add in the following code to the MyNote class:
public override bool Equals(object obj) {
if (obj == null) return false;
Part objAsNote = obj as MyNote;
if (objAsNote == null) return false;
else return Equals(objAsNote);
}
public bool Equals(MyNote otherNote) {
if(otherNote == null) return false;
return (this.ID.Equals(otherNote.ID));
}
public override int GetHashCode(){
return this.ID;
}
You can do something like this:
public ActionResult Edit(MyNote noteToEdit)
{
var oldNote = notes.FirstOrDefault(n => n.Id == noteToEdit.Id);
if(oldNote == null)
return View(); //With some error message;
oldNote.Title = noteToEdit.Title;
oldNote.OprettelsesDato = DateTime.Now;
oldNote.Note = noteToEdit.Note;
return RedirectToAction("Index", "Note");
}
public ActionResult Delete(int id)
{
var noteToRemove = notes.FirstOrDefault(x => x.Id == id);
if(noteToRemove == null)
return View(); //With some error message;
notes.Remove(noteToRemove);
return RedirectToAction("Index", "Note");
}
When you are editing your note, i recommend you to use AutoMapper to make your code more easy to maintain.

How can I log all entities change, during .SaveChanges() using EF code first?

I'm using EF code first. I'm using a base Repository for all my repositories and an IUnitofWork that inject to the repositories, too:
public interface IUnitOfWork : IDisposable
{
IDbSet<TEntity> Set<TEntity>() where TEntity : class;
int SaveChanges();
}
public class BaseRepository<T> where T : class
{
protected readonly DbContext _dbContext;
protected readonly IDbSet<T> _dbSet;
public BaseRepository(IUnitOfWork uow)
{
_dbContext = (DbContext)uow;
_dbSet = uow.Set<T>();
}
//other methods
}
e.g my OrderRepository is like this:
class OrderRepository: BaseRepository<Order>
{
IUnitOfWork _uow;
IDbSet<Order> _order;
public OrderRepository(IUnitOfWork uow)
: base(uow)
{
_uow = uow;
_order = _uow.Set<Order>();
}
//other methods
}
And I use it in this way:
public void Save(Order order)
{
using (IUnitOfWork uow = new MyDBContext())
{
OrderRepository repository = new OrderRepository(uow);
try
{
repository.ApplyChanges<Order>(order);
uow.SaveChanges();
}
}
}
Is there any way to log change histories of all entities(include their navigation properties) during .SaveChanges()? I want to log original values(before save occurs) and changed values(after save occurs).
You can get the before and after values for all changed entities by going through DbContext.ChangeTracker. Unfortunately the API is a little verbose:
var changeInfo = context.ChangeTracker.Entries()
.Where (t => t.State == EntityState.Modified)
.Select (t => new {
Original = t.OriginalValues.PropertyNames.ToDictionary (pn => pn, pn => t.OriginalValues[pn]),
Current = t.CurrentValues.PropertyNames.ToDictionary (pn => pn, pn => t.CurrentValues[pn]),
});
You can modify that to include things like the type of the entity if you need that for your logging. There is also a ToObject() method on the DbPropertyValues (the type of OriginalValues and CurrentValues) you could call if you already have a way to log whole objects, although the objects returned from that method will not have their navigation properties populated.
You can also modify that code to get all entities in the context by taking out the Where clause, if that makes more sense given your requirements.
I have overridded the default SaveChanges method to log changes for add/update/delete in entity. Though it does not cover navigation property changes.
Based on this article: Using entity framework for auditing
public int SaveChanges(string userId)
{
int objectsCount;
List<DbEntityEntry> newEntities = new List<DbEntityEntry>();
// Get all Added/Deleted/Modified entities (not Unmodified or Detached)
foreach (var entry in this.ChangeTracker.Entries().Where
(x => (x.State == System.Data.EntityState.Added) ||
(x.State == System.Data.EntityState.Deleted) ||
(x.State == System.Data.EntityState.Modified)))
{
if (entry.State == System.Data.EntityState.Added)
{
newEntities.Add(entry);
}
else
{
// For each changed record, get the audit record entries and add them
foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId))
{
this.AuditLogs.Add(changeDescription);
}
}
}
// Default save changes call to actually save changes to the database
objectsCount = base.SaveChanges();
// We don't have recordId for insert statements that's why we need to call this method again.
foreach (var entry in newEntities)
{
// For each changed record, get the audit record entries and add them
foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId, true))
{
this.AuditLogs.Add(changeDescription);
}
// TODO: Think about performance here. We are calling db twice for one insertion.
objectsCount += base.SaveChanges();
}
return objectsCount;
}
#endregion
#region Helper Methods
/// <summary>
/// Helper method to create record description for Audit table based on operation done on dbEntity
/// - Insert, Delete, Update
/// </summary>
/// <param name="dbEntity"></param>
/// <param name="userId"></param>
/// <returns></returns>
private List<AuditLog> GetAuditRecordsForEntity(DbEntityEntry dbEntity, string userId, bool insertSpecial = false)
{
List<AuditLog> changesCollection = new List<AuditLog>();
DateTime changeTime = DateTime.Now;
// Get Entity Type Name.
string tableName1 = dbEntity.GetTableName();
// http://stackoverflow.com/questions/2281972/how-to-get-a-list-of-properties-with-a-given-attribute
// Get primary key value (If we have more than one key column, this will need to be adjusted)
string primaryKeyName = dbEntity.GetAuditRecordKeyName();
int primaryKeyId = 0;
object primaryKeyValue;
if (dbEntity.State == System.Data.EntityState.Added || insertSpecial)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName, true);
if(primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
// For Inserts, just add the whole record
// If the dbEntity implements IDescribableEntity,
// use the description from Describe(), otherwise use ToString()
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_ADD,
TableName = tableName1,
RecordId = primaryKeyId, // Again, adjust this if you have a multi-column key
ColumnName = "ALL", // To show all column names have been changed
NewValue = (dbEntity.CurrentValues.ToObject() is IAuditableEntity) ?
(dbEntity.CurrentValues.ToObject() as IAuditableEntity).Describe() :
dbEntity.CurrentValues.ToObject().ToString()
}
);
}
else if (dbEntity.State == System.Data.EntityState.Deleted)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);
if (primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
// With deletes use whole record and get description from Describe() or ToString()
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_DELETE,
TableName = tableName1,
RecordId = primaryKeyId,
ColumnName = "ALL",
OriginalValue = (dbEntity.OriginalValues.ToObject() is IAuditableEntity) ?
(dbEntity.OriginalValues.ToObject() as IAuditableEntity).Describe() :
dbEntity.OriginalValues.ToObject().ToString()
});
}
else if (dbEntity.State == System.Data.EntityState.Modified)
{
primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);
if (primaryKeyValue != null)
{
Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
}
foreach (string propertyName in dbEntity.OriginalValues.PropertyNames)
{
// For updates, we only want to capture the columns that actually changed
if (!object.Equals(dbEntity.OriginalValues.GetValue<object>(propertyName),
dbEntity.CurrentValues.GetValue<object>(propertyName)))
{
changesCollection.Add(new AuditLog()
{
UserId = userId,
EventDate = changeTime,
EventType = ModelConstants.UPDATE_TYPE_MODIFY,
TableName = tableName1,
RecordId = primaryKeyId,
ColumnName = propertyName,
OriginalValue = dbEntity.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntity.OriginalValues.GetValue<object>(propertyName).ToString(),
NewValue = dbEntity.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntity.CurrentValues.GetValue<object>(propertyName).ToString()
}
);
}
}
}
// Otherwise, don't do anything, we don't care about Unchanged or Detached entities
return changesCollection;
}
you have scared people away with the extra requirement
Include their navigation properties
This is simply a non trivial exercise.
And if this is important, you should manage/track changes across references with code.
this is a sample covering this topic
Undo changes in entity framework entities
There is a sample doing close top what you want here
undo changes
It can easily be converted to load before and after images elsewhere.
Given the ObjectState entry after DetectChanges is called, you can implement a simple entity by entity option. and per UOW. But the navigation / references version makes this very complex as you worded the requirement.
EDIT : How to access the changeList
public class Repository<TPoco>{
/....
public DbEntityEntry<T> Entry(T entity) { return Context.Entry(entity); }
public virtual IList<ChangePair> GetChanges(object poco) {
var changes = new List<ObjectPair>();
var thePoco = (TPoco) poco;
foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
var curr = Entry(thePoco).CurrentValues[propName];
var orig = Entry(thePoco).OriginalValues[propName];
if (curr != null && orig != null) {
if (curr.Equals(orig)) {
continue;
}
}
if (curr == null && orig == null) {
continue;
}
var aChangePair = new ChangePair {Key = propName, Current = curr, Original = orig};
changes.Add(aChangePair);
}
return changes;
}
///... partial repository shown
}
// FYI the simple return structure
public class ChangePair {
public string Key { get; set; }
public object Original { get; set; }
public object Current { get; set; }
}
DbContext has ChangeTracker property.
You can override .SaveChanges() in your context and log changes.
I don't think that entity framework can do it for you. Probably, you must detect changes directly in your model classes.
I've expanded on Steve's answer to provide a check for Changed, Added, and Deleted entities and print them in a sensible way.
(My use case is to ensure there are no unsaved changes before disposing of a DbContext instance, but this check could be done at any point)
/// <summary>Helper method that checks whether the DbContext had any unsaved changes before it was disposed.</summary>
private void CheckForUnsavedChanges(DbContext dbContext)
{
try
{
List<DbEntityEntry> changedEntityEntries = dbContext.ChangeTracker.Entries()
.Where(t => t.State != EntityState.Unchanged && t.State != EntityState.Detached).ToList();
if (!changedEntityEntries.Any())
return;
throw new Exception("Detected that there were unsaved changes made using a DbContext. This could be due to a missing call to `.SaveChanges()` or possibly " +
"some read-only operations that modified the returned entities (in which case you might wish to use `.AsNoTracking()` in your query). Changes:\n " +
String.Join("\n ", changedEntityEntries.Select(entry => $"{entry.Entity.GetType()} {entry.State}:\n " + String.Join("\n ",
entry.State == EntityState.Modified ? entry.CurrentValues.PropertyNames
// Only output properties whose values have changed (and hope they have a good ToString() implementation)
.Where(pn => entry.OriginalValues?[pn] != entry.CurrentValues[pn])
.Select(pn => $"{pn} ({entry.OriginalValues?[pn]} -> {entry.CurrentValues[pn]})") :
// Added or Deleted entities are output in their entirety
entry.State == EntityState.Added ? entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.CurrentValues[pn]}") :
/* entry.State == EntityState.Deleted ? */ entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.OriginalValues[pn]}")))));
}
catch (Exception ex)
{
_logger.Error("Issue encountered when checking for unsaved changes.", ex);
}
}

Categories

Resources