How to log database table value changes programmatically in terms of auditing? - sql-server

May be this could be accomplished using a trigger and an audit log table in SQL server. Or perhaps it could be accomplished by overriding the SaveChanges() method in Entity Framework. My concern is how to write the code to get it done and which one will be efficient. Can anybody help me?

If you log changes through the code, alongside data, you can add additional information to the audit log such as IP, user info, client info, ... and it is really helpful. The downside would be if someone changes data directly via the database you cannot find out what data has changed and the performance of logging data change by audit log table is better. If you just need to capture data change and don't need to find out who and from where data has changed choose the database approach.
Here is an implementation to capture data changes by EF Core:
public interface IAuditableEntity
{
}
public class AuditLogEntity
{
public int Id { get; set; }
public string UserName { get; set; }
public DateTime CreateDate { get; set; }
public ChangeType ChangeType { get; set; }
public string EntityId { get; set; }
public string EntityName { get; set; }
public IEnumerable<EntityPropertyChange> Changes { get; set; }
}
public class EntityPropertyChange
{
public string PropertyName { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
}
public enum ChangeType
{
Add = 1,
Edit = 2,
Remove = 3
}
In DbContext:
public class RegistryDbContext : DbContext
{
private readonly IHttpContextAccessor _contextAccessor;
public RegistryDbContext(DbContextOptions<RegistryDbContext> options, IHttpContextAccessor contextAccessor) :
base(options)
{
_contextAccessor = contextAccessor;
}
public DbSet<AuditLogEntity> AuditLogs { get; set; }
public override int SaveChanges()
{
CaptureChanges();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
CaptureChanges();
return await base.SaveChangesAsync(cancellationToken);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditLogEntity>().Property(p => p.UserId).IsUnicode(false).HasMaxLength(36).IsRequired(false);
modelBuilder.Entity<AuditLogEntity>().Property(p => p.EntityId).IsUnicode(false).HasMaxLength(36).IsRequired();
modelBuilder.Entity<AuditLogEntity>().Property(p => p.EntityName).IsUnicode(false).HasMaxLength(256).IsRequired(false);
builder.Ignore(p => p.Changes);
modelBuilder.Entity<AuditLogEntity>()
.Property(p => p.Changes).IsUnicode().HasMaxLength(int.MaxValue).IsRequired(false)
.HasConversion(
changes => JsonConvert.SerializeObject(changes, Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include,
TypeNameHandling = TypeNameHandling.Auto
}),
changes => JsonConvert.DeserializeObject<IList<EntityPropertyChange>>(changes, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include,
TypeNameHandling = TypeNameHandling.Auto
}));
}
private void CaptureChanges()
{
var changes = ChangeTracker
.Entries<IAuditableEntity>()
.Where(e =>
e.State == EntityState.Added ||
e.State == EntityState.Modified ||
e.State == EntityState.Deleted)
.Select(GetAuditLogItems)
.ToList();
AuditLogs.AddRange(changes);
}
private AuditLogEntity GetAuditLogItems(EntityEntry entry)
{
var auditEntity = new AuditLogEntity
{
CreateDate = DateTime.Now,
EntityId = entry.Properties.FirstOrDefault(f => f.Metadata.IsPrimaryKey())?.CurrentValue?.ToString(),
EntityName = entry.Metadata.Name,
UserName = _contextAccessor?.HttpContext?.User?.Name.ToString(),
};
switch (entry.State)
{
case EntityState.Added:
auditEntity.ChangeType = ChangeType.Add;
auditEntity.Changes = GetChanges(entry.Properties, e => true).ToList();
foreach (var entityChange in auditEntity.Changes)
entityChange.OldValue = null;
break;
case EntityState.Modified:
auditEntity.ChangeType = ChangeType.Edit;
auditEntity.Changes = GetChanges(entry.Properties, e => e.IsModified).ToList();
break;
case EntityState.Deleted:
auditEntity.ChangeType = ChangeType.Remove;
break;
}
return auditEntity;
}
private IEnumerable<EntityPropertyChange> GetChanges(IEnumerable<PropertyEntry> properties,
Func<PropertyEntry, bool> predicate) => properties
.Where(predicate)
.Select(property =>
new EntityPropertyChange
{
PropertyName = property.Metadata.Name,
OldValue = property.OriginalValue?.ToString(),
NewValue = property.CurrentValue?.ToString()
});
}
I use IAuditableEntity (an empty interface) to mark entities that I want to capture changes.
public class CustomerEntity : IAuditableEntity
{
...
}
You can also use Audit.NET library to capture changes.

Related

Base entity common to all tables in EF Core 3.0

I would like to use a base entity common to all entities as every table should have ad ID, InsertDate and LastModifiedDate.
As per documentation I should create a BaseEntity abstract class and every entity should inheritance from this.
public abstract class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime? InsertDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? LastModifiedDateTime { get; set; }
}
Everything was ok until I started adding relations. Now after relations with foreign keys has been added the Migration is creating only one big table called BaseEntity with Discriminators but the abstract base entity should only be used to inheritance common properties.
I have read here that there are 3 type of inheritance but in EF Core 3.0 only TPH is available. Seeing examples on the web with abstract base class aren't having this problems.
I was wondering If I am missing something in my implementation if you guys please help me find out.
This:
modelBuilder.Entity<BaseEntity>()
declares BaseEntity as a database entity. Instead configure all the subtypes. Since they are mapped to seperate tables, they need seperate configuration. EG
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
if (et.ClrType.IsSubclassOf(typeof(BaseEntity)))
{
et.FindProperty("InsertDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("InsertDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd;
et.FindProperty("LastModifiedDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("LastModifiedDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
}
base.OnModelCreating(modelBuilder);
}
Note, that this won't cause the LastModifiedDateTime to be updated for every change. That would require a trigger or an interceptor of some kind in EF.
Here's another alternative.
public abstract class BaseEntityWithUpdatedAndRowVersion
{
[Display(Name = "Updated By", Description = "User who last updated this meeting.")]
[Editable(false)]
[ScaffoldColumn(false)]
public string UpdatedBy { get; set; }
[Display(Name = "Updated", Description = "Date and time this row was last updated.")]
[Editable(false)]
[ScaffoldColumn(false)]
public DateTimeOffset UpdatedDateTime { get; set; }
[Display(Name = "SQL Server Timestamp", ShortName = "RowVersion", Description = "Internal SQL Server row version stamp.")]
[Timestamp]
[Editable(false)]
[ScaffoldColumn(false)]
public byte[] RowVersion { get; set; }
}
}
And its abstract configuration:
internal abstract class BaseEntityWithUpdatedAndRowVersionConfiguration <TBase> : IEntityTypeConfiguration<TBase>
where TBase: BaseEntityWithUpdatedAndRowVersion
{
public virtual void Configure(EntityTypeBuilder<TBase> entity)
{
entity.Property(e => e.UpdatedBy)
.IsRequired()
.HasMaxLength(256);
entity.Property(e => e.UpdatedDateTime)
.HasColumnType("datetimeoffset(0)")
.HasDefaultValueSql("(sysdatetimeoffset())");
entity.Property(e => e.RowVersion)
.IsRequired()
.IsRowVersion();
}
}
Here's a concrete class using the base entity.
public partial class Invitation: BaseEntityWithUpdatedAndRowVersion, IValidatableObject
{
[Display(Name = "Paper", Description = "Paper being invited.")]
[Required]
public int PaperId { get; set; }
[Display(Name = "Full Registration Fee Waived?", ShortName = "Fee Waived?",
Description = "Is the registration fee completely waived for this author?")]
[Required]
public bool IsRegistrationFeeFullyWaived { get; set; }
}
And its configuration code, calling the base configuration:
internal class InvitationConfiguration : BaseEntityWithUpdatedAndRowVersionConfiguration<Invitation>
{
public override void Configure(EntityTypeBuilder<Invitation> entity)
{
base.Configure(entity);
entity.HasKey(e => e.PaperId);
entity.ToTable("Invitations", "Offerings");
entity.Property(e => e.PaperId).ValueGeneratedNever();
entity.HasOne(d => d.Paper)
.WithOne(p => p.Invitation)
.HasForeignKey<Invitation>(d => d.PaperId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("Invitations_FK_IsFor_Paper");
}
}
Finally, this addition to the db context handles the updated by and updated date/time.
public partial class ConferenceDbContext : IdentityDbContext<ConferenceUser, ConferenceRole, int>
{
public override int SaveChanges()
{
AssignUpdatedByAndTime();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
AssignUpdatedByAndTime();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(cancellationToken).ConfigureAwait(true);
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(true);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "App is not being globalized.")]
private void AssignUpdatedByAndTime()
{
//Get changed entities (added or modified).
ChangeTracker.DetectChanges();
var changedEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
//Assign UpdatedDateTime and UpdatedBy properties if they exist.
AssignUpdatedByUserAndTime(changedEntities);
}
#region AssignUpdated
/// <summary>Assign updated-by & updated-when to any entity containing those attributes, including general category parent entities.</summary>
private void AssignUpdatedByUserAndTime(List<EntityEntry> changedEntities)
{
foreach (EntityEntry entityEntry in changedEntities)
{
//Some subcategory entities have the updated date/by attributes in the parent entity.
EntityEntry parentEntry = null;
string entityTypeName = entityEntry.Metadata.Name; //Full class name, e.g., ConferenceEF.Models.Meeting
entityTypeName = entityTypeName.Split('.').Last();
switch (entityTypeName)
{
case "Paper":
case "FlashPresentation":
case "Break":
parentEntry = entityEntry.Reference(nameof(SessionItem)).TargetEntry;
break;
default:
break;
}
AssignUpdatedByUserAndTime(parentEntry ?? entityEntry);
}
}
private void AssignUpdatedByUserAndTime(EntityEntry entityEntry)
{
if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
|| entityEntry.Entity is PaperRating)
{
PropertyEntry updatedDateTime = entityEntry.Property("UpdatedDateTime");
DateTimeOffset? currentValue = (DateTimeOffset?)updatedDateTime.CurrentValue;
//Avoid possible loops by only updating time when it has changed by at least 1 minute.
//Is this necessary?
if (!currentValue.HasValue || currentValue < DateTimeOffset.Now.AddMinutes(-1))
updatedDateTime.CurrentValue = DateTimeOffset.Now;
if (entityEntry.Properties.Any(p => p.Metadata.Name == "UpdatedBy"))
{
PropertyEntry updatedBy = entityEntry.Property("UpdatedBy");
string newValue = CurrentUserName; //ClaimsPrincipal.Current?.Identity?.Name;
if (newValue == null && !updatedBy.Metadata.IsColumnNullable())
newValue = string.Empty;
if (updatedBy.CurrentValue?.ToString() != newValue)
updatedBy.CurrentValue = newValue;
}
}
}
#endregion AssignUpdated
}

ArgumentNullException: Value cannot be null. (Parameter 'source')

I have parent and childs nested tables.
Here is my model:
public class Categories
{
[Key]
public int CategoriesId { get; set; }
public int Order { get; set; }
public string CategoryName { get; set; }
public List<News> News { get; set; }
}
public class News
{
[Key]
public int NewsId { get; set; }
public int CategoriesId { get; set; }
public string Content { get; set; }
public DateTime Date { get; set; }
...
public List<Comments> Comments { get; set; }
public Categories Categories { get; set; }
}
public class Comments
{
[Key]
public int CommentsId { get; set; }
public int NewsId { get; set; }
public string Comment { get; set; }
...
public News News { get; set; }
}
public class NewsImages
{
[Key]
public int ImageId { get; set; }
public int NewsId { get; set; }
public string ImageUrl { get; set; }
public bool Cover { get; set;}
...
public News News { get; set; }
}
I'm trying to send it from ViewComponent to View;
public async Task<IViewComponentResult> InvokeAsync()
{
var group = _dbContext.Categories.Where(k => k.Order != 0).OrderBy(h => h.Order)
.Select(c => new
{
C = c,
N = c.News.OrderByDescending(n => n.Date).Take(5)
.Select(r => new
{
Y = r.Comments,
R = r.NewsImages.Where(rs => rs.Cover == true).FirstOrDefault()
})
});
var model = group
.Select(m => m.C);
return View(await model.ToListAsync()) ;
}
I am sure there are enough News records for every Category, But I get error :
ArgumentNullException: Value cannot be null. (Parameter 'source')
AspNetCore.Views_Shared_Components_IndexKategori_Default.ExecuteAsync() in Default.cshtml
var bp = k.News.FirstOrDefault();
if I use that code works fine :
var model = _dbContext.Categories
.Include(h => h.News).ThenInclude(h => h.Comments)
.Include(h => h.News).ThenInclude(h => h.NewsImages)
.Where(h => h.Order != 0)
.OrderBy(h => h.Order)
But when I use the code above, a few records appear for some categories, and some categories react as if there are no records.
Where am I making mistakes?
Thank you in advance for those who helped ..
Whenever you have a big LINQ statement that throws an exception, and you can't find where the exception comes form, translate the LINQ into smaller steps, and ToList() every step.
public async Task<IViewComponentResult> InvokeAsync()
{
// Temp code: small steps, ToList after every step
var a = dbContext.Categories.Where(category => category.Order != 0).ToList();
var b = a.OrderBy(category => category.Order).ToList();
var c = b.Select(category => new
{
Category = category,
News = category.News.OrderByDescending(news => news.Date)
.Take(5)
.ToList();
})
.ToList();
var d = c.Select(item => new
{
Category = item.Category,
NewsItems = item.News.Select(news => new
{
Comments = news.Comments,
Images = news.NewsImages.Where(newsImage => newsImage.Cover).ToList(),
})
.ToList(),
})
.ToList();
var e = d.Select(item => new
{
Category = item.Category,
NewsItems = item.NewsItems.Select(newsItem => new
{
Comments = newsItem.Comments,
Images = images.FirstOrDefault();
})
.ToList(),
})
.ToList();
// original code:
var group = _dbContext.Categories.Where...
}
I'm sure that your debugger will tell you which step is incorrect.

How can I auto-update the int ModifiedBy property on a Entity with UserId in Entity Framework 4 when saving?

I am using Simple Membership and a UserProfile table that maintains UserId and UserName:
public partial class UserProfile
{
public UserProfile()
{
this.webpages_Roles = new List<webpages_Roles>();
}
public int UserId { get; set; }
public string UserName { get; set; }
public virtual ICollection<webpages_Roles> webpages_Roles { get; set; }
}
With Entity Framework I am running the following which is inside my Context:
public partial class UowContext : DbContext
// code to set up DbSets here ...
public DbSet<Content> Contents { get; set; }
private void ApplyRules()
{
var r1 = new Random();
var r2 = new Random();
foreach (var entry in this.ChangeTracker.Entries()
.Where(
e => e.Entity is IAuditableTable &&
(e.State == EntityState.Added) ||
(e.State == EntityState.Modified)))
{
IAuditableTable e = (IAuditableTable)entry.Entity;
if (entry.State == EntityState.Added)
{
e.CreatedBy = // I want to put the integer value of UserId here
e.CreatedDate = DateTime.Now;
}
e.ModifiedBy = // I want to put the integer value of UserId here
e.ModifiedDate = DateTime.Now;
}
}
Here is the schema showing how user information is stored. Note that I store the integer UserId and not the UserName in the tables:
public abstract class AuditableTable : IAuditableTable
{
public virtual byte[] Version { get; set; }
public int CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public int ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
}
Here's an example of a controller action that I use:
public HttpResponseMessage PostContent(Content content)
{
try
{
_uow.Contents.Add(content);
_uow.Commit();
var response = Request.CreateResponse<Content>(HttpStatusCode.Created, content);
return response;
}
catch (DbUpdateException ex)
{
return Request.CreateErrorResponse(HttpStatusCode.Conflict, ex);
}
}
I then have:
public class UowBase : IUow, IDisposable
{
public UowBase(IRepositoryProvider repositoryProvider)
{
CreateDbContext();
repositoryProvider.DbContext = DbContext;
RepositoryProvider = repositoryProvider;
}
public IRepository<Content> Contents { get { return GetStandardRepo<Content>(); } }
and:
public class GenericRepository<T> : IRepository<T> where T : class
{
public GenericRepository(DbContext dbContext)
{
if (dbContext == null)
throw new ArgumentNullException("An instance of DbContext is required to use this repository", "context");
DbContext = dbContext;
DbSet = DbContext.Set<T>();
}
public virtual void Add(T entity)
{
DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
if (dbEntityEntry.State != EntityState.Detached)
{
dbEntityEntry.State = EntityState.Added;
}
else
{
DbSet.Add(entity);
}
}
How can I determine the UserId from inside of my Context so I can populate the Id in my tables?
In Code you will have UserName with you through:
HttpContext.Current.User.Identity.Name
you can than query UserProfile table against that Name and get the UserId from there and than assign it to ModifiedBy attribute.
Make sure that you query UserProfile table outside the foreach loop :)

MVC Persist Collection ViewModel (Update, Delete, Insert)

In order to create a more elegant solution I'm curios to know your suggestion about a solution to persist a collection.
I've a collection stored on DB.
This collection go to a webpage in a viewmodel.
When the go back from the webpage to the controller I need to persist the modified collection to the same DB.
The simple solution is to delete the stored collection and recreate all rows.
I need a more elegant solution to mix the collections and delete not present record, update similar records ad insert new rows.
this is my Models and ViewModels.
public class CustomerModel
{
public virtual string Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<PreferredAirportModel> PreferedAirports { get; set; }
}
public class AirportModel
{
public virtual string Id { get; set; }
public virtual string AirportName { get; set; }
}
public class PreferredAirportModel
{
public virtual AirportModel Airport { get; set; }
public virtual int CheckInMinutes { get; set; }
}
// ViewModels
public class CustomerViewModel
{
[Required]
public virtual string Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<PreferredAirporViewtModel> PreferedAirports { get; set; }
}
public class PreferredAirporViewtModel
{
[Required]
public virtual string AirportId { get; set; }
[Required]
public virtual int CheckInMinutes { get; set; }
}
And this is the controller with not elegant solution.
public class CustomerController
{
public ActionResult Save(string id, CustomerViewModel viewModel)
{
var session = SessionFactory.CurrentSession;
var customer = session.Query<CustomerModel>().SingleOrDefault(el => el.Id == id);
customer.Name = viewModel.Name;
// How can I Merge collections handling delete, update and inserts ?
var modifiedPreferedAirports = new List<PreferredAirportModel>();
var modifiedPreferedAirportsVm = new List<PreferredAirporViewtModel>();
// Update every common Airport
foreach (var airport in viewModel.PreferedAirports)
{
foreach (var custPa in customer.PreferedAirports)
{
if (custPa.Airport.Id == airport.AirportId)
{
modifiedPreferedAirports.Add(custPa);
modifiedPreferedAirportsVm.Add(airport);
custPa.CheckInMinutes = airport.CheckInMinutes;
}
}
}
// Remove common airports from ViewModel
modifiedPreferedAirportsVm.ForEach(el => viewModel.PreferedAirports.Remove(el));
// Remove deleted airports from model
var toDelete = customer.PreferedAirports.Except(modifiedPreferedAirports);
toDelete.ForEach(el => customer.PreferedAirports.Remove(el));
// Add new Airports
var toAdd = viewModel.PreferedAirports.Select(el => new PreferredAirportModel
{
Airport =
session.Query<AirportModel>().
SingleOrDefault(a => a.Id == el.AirportId),
CheckInMinutes = el.CheckInMinutes
});
toAdd.ForEach(el => customer.PreferedAirports.Add(el));
session.Save(customer);
return View();
}
}
My environment is ASP.NET MVC 4, nHibernate, Automapper, SQL Server.
Well, if "elegant" is just "don't clear and recreate all" (untested) :
var airports = customer.PreferedAirports;
var viewModelAirports = viewModel.PreferredAirports;
foreach (var airport in airports) {
//modify common airports
var viewModelAirport = viewModelAirports.FirstOrDefault(m => m.AirportId == airport.AirportId);
if (viewModelAirport != null) {
airport.X = viewModelAirport.X;
airport.Z = viewModelAirport.Z;
//remove commonAirports from List
viewModelAirports.Remove(viewModelAirport);
continue;
}
//delete airports not present in ViewModel
customer.PreferedAirports.Remove(airport);
}
//add new airports
foreach (var viewModelAirport in viewModelAirports) {
customer.PreferedAirports.Add(new PreferredAirportModel {
Airport = session.Query<AirportModel>().SingleOrDefault(a => a.Id == el.AirportId),
CheckInMinutes = el.CheckInMinutes
});
}
session.Save(customer);

Ria Service: Navigation Property is null

For example I have two entities
Class A
{
public Guid Id {get;set;}
public Guid BId {get;set;}
public B InstanceB {get;set;}
}
Class B
{
public Guid Id {get;set}
}
B is related to A, on my silver light application I am creating a new instance of A, and also a new instance of B. The new instance of B does not exist yet. But I need the instance of B on my service.
Can I do this without Entity or Association with Ria Service?
Edit:
My Class A :
public partial class lSync{
// Metadata classes are not meant to be instantiated.
private lSync() {
}
public string ConflictMessage { get; set; }
public DateTime DateInserted { get; set; }
public Guid vValuesId { get; set; }
public Guid ID { get; set; }
public bool IsConflict { get; set; }
public bool IsReadyToSync { get; set; }
public Guid SyncSet { get; set; }
public vValues vValues { get; set; }
}
My Ria Service:
[Invoke]
public lSync[] SynchvValuesFromClient(lSync[] syncs) {
bool noConflict = true;
foreach (lSync sync in syncs) {
var servervValue = GetvValuesByID(sync.vValuesId).FirstOrDefault();
var queuevValues = sync.vValues; //sync.vValues here is null, but my sync.vValuesId is not
if (servervValue== null) {
InsertvValues(queueValue);
}
else {
if (servervValue.IsServerConflict(queueValue)) {
sync.IsConflict = true;
sync.ConflictMessage = "Conflict";
noConflict = false;
break;
}
if (!servervValue.AreValuesEqual(queueValue)) {
UpdatevValues(queueValue);
}
}
}
if (noConflict) {
this.ObjectContext.SaveChanges();
}
return syncs;
}
public IQueryable<vValues> GetvValuesByID(Guid ID) {
return ObjectContext.vValues.Where(t => t.ID == ID);
}
public void InsertvValues(vValues model) {
model.ServerDate = DateTime.UtcNow;
if ((model.EntityState != EntityState.Detached)) {
this.ObjectContext.ObjectStateManager.ChangeObjectState(model, EntityState.Added);
}
else {
this.ObjectContext.vValues.AddObject(model);
}
}
public void UpdatevValues(vValuesmodel) {
model.ServerDate = DateTime.UtcNow;
this.ObjectContext.vValues.AttachAsModified(model, this.ChangeSet.GetOriginal(model));
}
:(
Edit
The order is wrong of your method :)
Make an instance of the service before creating instances of the Entities.
It should be:
public void SyncToServer() {
ContextService service = new ContextService();
var instanceA = new A();
instanceA.InstanceB = new B();
service.SubmitChanges(); //service.SaveChanges() for LinqToEntities
}
Are you reloading after a submit because only adding the [Include] attribute in the DomainService MetaData won't work. You need to do this in the DomainService for LinqToSql
public A GetA()
{
DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<A>(a => a.InstanceB);
this.DataContext.LoadOptions = dlo;
return this.DataContext.APlural.FirstOrDefault( ); //don't know the plural of A.
}
LinqToEntities:
public A GetA()
{
return this.MyEntitiesContext.APlural.Include( "instanceB" ).FirstOrDefault( ); //don't know the plural of A.
}
var a = new A(){
B = new B(); //or (B)selectedItem
}
now a.Id and a.BId is 0 until you SaveChanged and return saved A

Resources