How do you deal with race between two client upserts? - c#

I'm writing a simple messaging module so one process can publish messages and another can subscribe to them. I'm using EF/SqlServer as the out of process communication mechanism. A "Server" is just a name that a publisher/subscriber pair have in common (could have been called a "Channel").
I have the following method which adds a row to the database representing a named "Server"
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Servers.Add(new Server { Name = name });
}
}
The problem I'm having is that when I start two clients at the same time, only one is supposed to add a new Server entry, however, that is not how it's working out. I'm actually getting the very wrong result of two entries with the same name, and realizing that an Any() guard is not sufficient for this.
The Entity for Server uses an int PK and supposedly my repository would enforce the uniqueness of the Name field. I'm starting to think this isn't going to work though.
public class Server
{
public int Id { get; set; }
public string Name { get; set; }
}
The two ways I think I could fix this both seem less than ideal:
String primary keys
Ignoring Exception
This is the issue of concurrency, right?
How can I deal with it in this situation where I want two clients to call the repository with the same Name but get a result of only one row with that name in the database?
Update: Here is the Repository Code
namespace MyBus.Data
{
public class Repository : IDisposable
{
private readonly Context context;
private readonly bool autoSave;
public delegate Chain Chain(Action<Repository> action);
public static Chain Command(Action<Repository> action)
{
using (var repo = new Data.Repository(true))
{
action(repo);
}
return new Chain(next => Command(next));
}
public Repository(bool autoSave)
{
this.autoSave = autoSave;
context = new Context();
}
public void Dispose()
{
if (autoSave)
context.SaveChanges();
context.Dispose();
}
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Servers.Add(new Server { Name = name });
}
}
public void AddClient(string name, bool isPublisher)
{
if (!context.Clients.Any(c => c.Name == name))
{
context.Clients.Add(new Client
{
Name = name,
ClientType = isPublisher ? ClientType.Publisher : ClientType.Subscriber
});
}
}
public void AddMessageType<T>()
{
var typeName = typeof(T).FullName;
if (!context.MessageTypes.Any(c => c.Name == typeName))
{
context.MessageTypes.Add(new MessageType { Name = typeName });
}
}
public void AddRegistration<T>(string serverName, string clientName)
{
var server = context.Servers.Single(c => c.Name == serverName);
var client = context.Clients.Single(c => c.Name == clientName);
var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
if (!context.Registrations.Any(c =>
c.ServerId == server.Id &&
c.ClientId == client.Id &&
c.MessageTypeId == messageType.Id))
{
context.Registrations.Add(new Registration
{
Client = client,
Server = server,
MessageType = messageType
});
}
}
public void AddMessage<T>(T item, out int messageId)
{
var messageType = context.MessageTypes.Single(c => c.Name == typeof(T).FullName);
var serializer = new XmlSerializer(typeof(T));
var sb = new StringBuilder();
using (var sw = new StringWriter(sb))
{
serializer.Serialize(sw, item);
}
var message = new Message
{
MessageType = messageType,
Created = DateTime.UtcNow,
Data = sb.ToString()
};
context.Messages.Add(message);
context.SaveChanges();
messageId = message.Id;
}
public void CreateDeliveries<T>(int messageId, string serverName, string sendingClientName, T item)
{
var messageType = typeof(T).FullName;
var query = from reg in context.Registrations
where reg.Server.Name == serverName &&
reg.Client.ClientType == ClientType.Subscriber &&
reg.MessageType.Name == messageType
select new
{
reg.ClientId
};
var senderClientId = context.Clients.Single(c => c.Name == sendingClientName).Id;
foreach (var reg in query)
{
context.Deliveries.Add(new Delivery
{
SenderClientId = senderClientId,
ReceiverClientId = reg.ClientId,
MessageId = messageId,
Updated = DateTime.UtcNow,
DeliveryStatus = DeliveryStatus.Sent
});
}
}
public List<T> GetDeliveries<T>(string serverName, string clientName, out List<int> messageIds)
{
messageIds = new List<int>();
var messages = new List<T>();
var clientId = context.Clients.Single(c => c.Name == clientName).Id;
var query = from del in context.Deliveries
where del.ReceiverClientId == clientId &&
del.DeliveryStatus == DeliveryStatus.Sent
select new
{
del.Id,
del.Message.Data
};
foreach (var item in query)
{
var serializer = new XmlSerializer(typeof(T));
using (var sr = new StringReader(item.Data))
{
var t = (T)serializer.Deserialize(sr);
messages.Add(t);
messageIds.Add(item.Id);
}
}
return messages;
}
public void ConfirmDelivery(int deliveryId)
{
using (var context = new Context())
{
context.Deliveries.First(c => c.Id == deliveryId).DeliveryStatus = DeliveryStatus.Received;
context.SaveChanges();
}
}
}
}

You could keep the int primary key, but also define a unique index on the Name column.
This way, in concurrency situations only the first insert would be successful; any subsequent clients that attempt to insert the same server name would fail with an SqlException.

I'm currently using this solution:
public void AddServer(string name)
{
if (!context.Servers.Any(c => c.Name == name))
{
context.Database.ExecuteSqlCommand(#"MERGE Servers WITH (HOLDLOCK) AS T
USING (SELECT {0} AS Name) AS S
ON T.Name = S.Name
WHEN NOT MATCHED THEN
INSERT (Name) VALUES ({0});", name);
}
}

As an exercise in thoroughness I (think I) solved this problem another way, which preserves the type safety of the EF context but adds a bit of complexity:
First, this post, I learned how to add a unique constraint to the Server table:
Here's the Context code:
public class Context : DbContext
{
public DbSet<MessageType> MessageTypes { get; set; }
public DbSet<Message> Messages { get; set; }
public DbSet<Delivery> Deliveries { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Server> Servers { get; set; }
public DbSet<Registration> Registrations { get; set; }
public class Initializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
context.Database.Delete();
if (!context.Database.Exists())
{
context.Database.Create();
context.Database.ExecuteSqlCommand(
#"alter table Servers
add constraint UniqueServerName unique (Name)");
}
}
}
}
Now I need a way to selectively ignore exception when saving. I did this by adding the following members to my repository:
readonly List<Func<Exception, bool>> ExceptionsIgnoredOnSave =
new List<Func<Exception, bool>>();
static readonly Func<Exception, bool> UniqueConstraintViolation =
e => e.AnyMessageContains("Violation of UNIQUE KEY constraint");
Along with a new extension method to loop keep from depending on the position of the text in the inner exception chain:
public static class Ext
{
public static bool AnyMessageContains(this Exception ex, string text)
{
while (ex != null)
{
if(ex.Message.Contains(text))
return true;
ex = ex.InnerException;
}
return false;
}
}
And I modified the Dispose method of my Repository to check if the exception should be ignored or re-thrown:
public void Dispose()
{
if (autoSave)
{
try
{
context.SaveChanges();
}
catch (Exception ex)
{
if(!ExceptionsIgnoredOnSave.Any(pass => pass(ex)))
throw;
Console.WriteLine("ignoring exception..."); // temp
}
}
context.Dispose();
}
Finally, in the method which invokes the Add, I add the acceptable condition to the list:
public void AddServer(string name)
{
ExceptionsIgnoredOnSave.Add(UniqueConstraintViolation);
if (!context.Servers.Any(c => c.Name == name))
{
var server = context.Servers.Add(new Server { Name = name });
}
}

Related

trying to add different types inside if statement

I have a below method where I am loop through the list of id's and getting the data from db based on id and then creating the material and then adding to material list
public Construction AddToOsm(Model model, APIDbContext dbContext)
{
var construction = new Construction(model);
var surfaceType = dbContext.IntendedSurfaceTypes.SingleOrDefault(s => s.Id == this.SurfaceTypeId);
construction.setName(surfaceType?.Name);
using var materials = new MaterialVector();
var fenestrationMaterialById = new Dictionary<Guid, FenestrationMaterial>();
var opaqueMaterialById = new Dictionary<Guid, StandardOpaqueMaterial>();
foreach (var materialId in this.LayerIds.Where(i => i != default))
{
var opaqueMaterial = dbContext.OpaqueMaterials.SingleOrDefault(o => o.Id == materialId);
if (opaqueMaterial != default)
{
materials.Add(opaqueMaterialById.GetOrCreate(opaqueMaterial.Id, () => opaqueMaterial.AddToOsm(model)));
}
else
{
var glazingMaterial = dbContext.GlazingMaterials.SingleOrDefault(o => o.Id == materialId);
if (glazingMaterial != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(glazingMaterial.Id, () => glazingMaterial.AddToOsm(model)));
}
else
{
var glazingSimpleMaterial = dbContext.SimpleGlazingMaterials.SingleOrDefault(s => s.Id == materialId);
if(glazingSimpleMaterial != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(glazingSimpleMaterial.Id, () => glazingSimpleMaterial.AddToOsm(model)));
}
else
{
var gasGlazingMaterials = dbContext.GasGlazingMaterials.SingleOrDefault(a => a.Id == materialId);
if(gasGlazingMaterials != default)
{
materials.Add(fenestrationMaterialById.GetOrCreate(gasGlazingMaterials.Id, () => gasGlazingMaterials.AddToOsm(model)));
}
}
}
}
}
construction.setLayers(materials);
return construction;
}
I am looking a way to avoid this much of if-else statements mainly refactoring this but could not find a way to do. Could any one please suggest any idea on how to achieve the same.
Thanks in advance.
update: sample entity structure
public class GasGlazingMaterial : ISourceOfData, IIdentity<Guid>
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[ForeignKey("SourceOfData")]
public Guid? SourceOfDataId { get; set; }
public virtual CodeStandardGuideline SourceOfData { get; set; }
......
.....
}
A simple fix would be to "continue" after each materials.add. This would mean you dont need to embed the rest in an else

Inline (override) mapping for nested types with AutoMapper

I would like to use AutoMapper and provide some values in runtime.
For example I have DTO & ViewModel.
One of the property doesn't exist in DTO and cannot be mapped directly by using Converters/Resolvers/Transformers;
namespace Lab.So.Sample
{
public class UserDto
{
public int UserId { get; set; }
public string UserCode { get; set; }
}
public class UserGroupDto
{
public List<UserDto> Users { get; set; }
}
public class UserViewModel
{
public int UserId { get; set; }
public string FullName { get; set; }
}
public class UserGroup
{
public List<UserViewModel> Users { get; set; }
}
class Program
{
static void Main(string[] args)
{
var src = new Fixture().Create<UserGroupDto>();
Mapper.Initialize(cfg =>
{
cfg.CreateMissingTypeMaps = true;
cfg.ValidateInlineMaps = false;
});
// How to hook mapping of nested object User->UserViewModel to provide value of FullName in runtime
var dst = Mapper.Map<UserGroupDto, UserGroup>(src);
}
}
}
I have some workaround how do this, but it's not human-friendly, as for me:
class Program
{
internal class UserViewModelFullNameResolver : IValueResolver<UserDto, UserViewModel, string>
{
public string Resolve(UserDto source, UserViewModel destination, string destMember, ResolutionContext context)
{
var names = context.Items["ctx.Names"] as IDictionary<int, string>;
if (names == null || !names.TryGetValue(source.UserId, out var fullName))
{
return null;
}
return fullName;
}
}
static void Main(string[] args)
{
var src = new Fixture().Create<UserGroupDto>();
Mapper.Initialize(cfg =>
{
cfg.CreateMissingTypeMaps = true;
cfg.ValidateInlineMaps = false;
cfg.CreateMap<UserDto, UserViewModel>()
.ForMember(d => d.FullName, opt => opt.MapFrom<UserViewModelFullNameResolver>());
});
var names = new Dictionary<int, string>
{
{ 10, "FullName-10" },
{ 20, "FullName-20" },
};
var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Items["ctx.Names"] = names);
}
}
In that workaround most inconvenient it's agreement of key names in opt.Items;
If something accidentally made typo it hard to investigate and fix;
I looking something like this:
var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Use(new UserViewModelFullNameResolver());
By other words, its defining an instance of resolver in runtime for each unique case;
Also I would accept, if I will have ability to define a hook to mapping particular type in graph of objects:
var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Hook<UserDto,UserViewModel>((s,d)=> { /* any logic to read external data */ });
An example of usage:
var srcA = readDataA();
var srcB = readDataB();
var dst = Mapper.Map<UserGroupDto, UserGroup>(
src,
opt=>opt.Hook<UserDto,UserViewModel>(
(s,d)=>
{
d.FullName = srcA + srcB;
});
Please suggest something, that would help to read data to accomplish mapping in cases when source doesn't have all required data for nested object in destination.
I was able to use existing API to archive my goal:
public static class InlineMappingExtensions
{
public static void EnableInlineMapping(this IMapperConfigurationExpression cfg)
{
cfg.ForAllMaps((t, i) =>
{
i.AfterMap((s, d, ctx) =>
{
ctx.ApplyInlineMap(s, d);
}
);
});
}
public static IMappingOperationOptions OnMap<TSrc, TDst>(this IMappingOperationOptions opts,
Action<TSrc, TDst> resolve)
{
var srcTypeName = typeof(TSrc);
var dstTypeName = typeof(TDst);
var ctxKey = $"OnMap_{srcTypeName}_{dstTypeName}";
opts.Items.Add(ctxKey, resolve);
return opts;
}
private static void ApplyInlineMap(this ResolutionContext opts, object src, object dst)
{
if (src == null)
{
return;
}
if (dst == null)
{
return;
}
var srcTypeName = src.GetType();
var dstTypeName = dst.GetType();
var ctxKey = $"OnMap_{srcTypeName}_{dstTypeName}";
if (!opts.Items.TryGetValue(ctxKey, out var inlineMap))
{
return;
}
var act = inlineMap as Delegate;
act?.DynamicInvoke(src, dst);
}
}
To enable it:
Mapper.Initialize(cfg =>
{
cfg.CreateMissingTypeMaps = true;
cfg.ValidateInlineMaps = false;
cfg.EnableInlineMapping();
});
Sample of usage:
// read data from external sources
var names = new Dictionary<int, string>
{
{ 10, "FullName-10" },
{ 20, "FullName-20" },
};
Action<UserDto, UserViewModel> mapA = (s, d) =>
{
if (names.TryGetValue(s.UserId, out var name))
{
d.FullName = name;
}
};
Action<UserGroupDto, UserGroup> mapB = (s, d) =>
{
if (DateTime.Now.Ticks > 0)
{
d.Users = null;
}
};
var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt => opt.OnMap(mapA).OnMap(mapB));

Check for existing record in ICustomValidate of ASP.NET Boilerplate

For ICustomValidate in ASP.NET Boilerplate, we can validate the value for the field.
I am wondering whether it is able and recommended to check whether the added name of the Student already exists, in the ICustomValidate.
For example, when creating a new student, we will check whether the student with the same name already exists. Can we move this logic to ICustomValidate?
You can:
public class CreateStudentDto : ICustomValidate
{
public string Name { get; set; }
public void AddValidationErrors(CustomValidationContext context)
{
using (var scope = context.IocResolver.CreateScope())
{
using (var uow = scope.Resolve<IUnitOfWorkManager>().Begin())
{
var studentRepository = scope.Resolve<IRepository<Student, long>>();
var nameExists = studentRepository.GetAll()
.Where(s => s.Name == Name)
.Any();
if (nameExists)
{
var key = "A student with the same name already exists";
var errorMessage = context.Localize("sourceName", key);
var memberNames = new[] { nameof(Name) };
context.Results.Add(new ValidationResult(errorMessage, memberNames));
}
uow.Complete();
}
}
}
}
But such validation is usually done in a domain manager, e.g. AbpUserManager
Custom Validation in the DTO would be recommended for invariant conditions:
public class CreateTaskInput : ICustomValidate
{
public int? AssignedPersonId { get; set; }
public bool SendEmailToAssignedPerson { get; set; }
public void AddValidationErrors(CustomValidatationContext context)
{
if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0))
{
var errorMessage = "AssignedPersonId must be set if SendEmailToAssignedPerson is true!";
context.Results.Add(new ValidationResult(errorMessage));
}
}
}

Why do I keep getting an error on converting a generic list to the same list type?

I am running into this error
cannot convert from 'System.Collections.Generic.List' to 'HWC.DataAccess.FAREmailList' HWC.DataAccess
I am not understanding this error because its the same list type
here is the method that I am using
public void PrepareToSendEmailFromFAR(int id)
{
HWC = new HWCEntities();
FileAReport far = HWC.FileAReports.Where(w => w.FileAReportID == id).FirstOrDefault();
List<FAREmailList> emailList = null;
if(far.DistrictID != 0)
{
emailList = new List<FAREmailList>();
var query = from dcx in HWC.DistrictContactXREFs
where
dcx.DistrictID == far.DistrictID
select new
{
dcx.ContactID,
dcx.Contact.ContactEmail,
dcx.Contact.ContactName
};
foreach(var a in query)
{
emailList.Add(new FAREmailList
{
ContactName = a.ContactName,
EmailAddress = a.ContactEmail
});
}
SendEmailFromFAR(emailList);
}
if(far.DistrictID == 0)
{
emailList = new List<FAREmailList>();
var query = from dcx in HWC.DistrictContactXREFs
join d in HWC.Districts on dcx.DistrictID equals d.DistrictID into d_join
from d in d_join.DefaultIfEmpty()
join sp in HWC.StateProvinces on new { StateProvinceID = d.StateID } equals new { StateProvinceID = sp.StateProvinceID }
where
d.StateID == far.StateCountyID
select new
{
dcx.ContactID,
dcx.Contact.ContactEmail,
dcx.Contact.ContactName
};
foreach (var a in query)
{
emailList.Add(new FAREmailList
{
ContactName = a.ContactName,
EmailAddress = a.ContactEmail
});
}
SendEmailFromFAR(emailList);
}
}
and here is the method thats receiving the emailList
public void SendEmailFromFAR(FAREmailList el)
{
}
the data class is
public class FAREmailList
{
public string ContactName { get; set; }
public string EmailAddress { get; set; }
}
the error is being thrown at
SendEmailFromFAR(emailList);
I am not seeing what the issue is, is it because this is all in the same class file?
The error makes sense to me. emailList is of type List<FAREmailList> and SendEmailFromFAR takes a FAREmailList as input.

DHTMLX Scheduler recurring events

I have trouble with the DHTMLX scheduler specifically around recurring events.
I have tried to follow the documentation found here http://blog.scheduler-net.com/post/recurring-events-calendar-view-asp-net.aspx. However can't seem to get it working.
I can create the basic scheduler without any issues. The issue I now have is that any event that gets created won't save to the DB. This is what I have so far.
Model:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DHXJson(Alias = "id")]
public int Id { get; set; }
[DHXJson(Alias = "text")]
public string Description { get; set; }
[DHXJson(Alias = "start_date")]
public DateTime StartDate { get; set; }
[DHXJson(Alias = "end_date")]
public DateTime EndDate { get; set; }
[DHXJson(Alias="event_length")]
public int event_length { get; set; }
[DHXJson(Alias = "rec_type")]
public string rec_type { get; set; }
[DHXJson(Alias = "event_pid")]
public int event_pid { get; set; }
Controller:
public ActionResult Save(int? id, FormCollection actionValues)
{
var action = new DataAction(actionValues);
ApplicationDbContext data = new ApplicationDbContext();
try
{
var changedEvent = (Appointment)DHXEventsHelper.Bind(typeof(Appointment), actionValues);
//operations with recurring events require some additional handling
bool isFinished = deleteRelated(action, changedEvent, data);
if (!isFinished)
{
switch (action.Type)
{
case DataActionTypes.Insert:
data.Appointment.Add(changedEvent);
if (changedEvent.rec_type == "none")//delete one event from the serie
action.Type = DataActionTypes.Delete;
break;
case DataActionTypes.Delete:
changedEvent = data.Appointment.SingleOrDefault(ev => ev.Id == action.SourceId);
data.Appointment.Remove(changedEvent);
break;
default:// "update"
var eventToUpdate = data.Appointment.SingleOrDefault(ev => ev.Id == action.SourceId);
DHXEventsHelper.Update(eventToUpdate, changedEvent, new List<string>() { "id" });
break;
}
}
data.SaveChanges();
action.TargetId = changedEvent.Id;
}
catch
{
action.Type = DataActionTypes.Error;
}
return (new AjaxSaveResponse(action));
}
protected bool deleteRelated(DataAction action, Appointment changedEvent, ApplicationDbContext context)
{
bool finished = false;
if ((action.Type == DataActionTypes.Delete || action.Type == DataActionTypes.Update) && !string.IsNullOrEmpty(changedEvent.rec_type))
{
// context.Recurrings.DeleteAllOnSubmit(from ev in context.Recurrings where ev.event_pid == changedEvent.id select ev);
}
if (action.Type == DataActionTypes.Delete && (changedEvent.event_pid != 0 && changedEvent.event_pid != null))
{
// Recurring changed = (from ev in context.Recurrings where ev.id == action.TargetId select ev).Single();
// changed.rec_type = "none";
finished = true;
}
return finished;
}
Any help or ideas?
Try changing the change save method return value to "ContentResult". Also Look into your ApplicationDbContext and see if you can pull some hard-coded db values from that table when your index loads. Here is a copy of mine. I had the same problems until I used linq to classes to create the model/EF and my context is based on that. I was having the same issue when I created my own "light-weight" interface because I didn't want to use EF.
public ContentResult Save(int? id, FormCollection actionValues)
{
var action = new DataAction(actionValues);
var context = new SchedulerDataContext();
Int64 source_id = Int64.Parse(actionValues["id"]);
try
{
var changedDelEvent = (Delivery)DHXEventsHelper.Bind(typeof(Delivery), actionValues);
var changedRecEvent = (Recurring)DHXEventsHelper.Bind(typeof(Recurring), actionValues);
//operations with recurring events require some additional handling
bool isFinished = deleteRelated(action, changedRecEvent, context);
if (!isFinished)
{
switch (action.Type)
{
case DataActionTypes.Insert:
context.Recurrings.InsertOnSubmit(changedRecEvent);
context.SubmitChanges();
break;
case DataActionTypes.Delete:
changedRecEvent = context.Recurrings.SingleOrDefault(d => d.id == source_id);
if (changedRecEvent != null)
{
context.Recurrings.DeleteOnSubmit(changedRecEvent);
}
context.SubmitChanges();
break;
default:// "update"
var eventToUpdate = context.Deliveries.SingleOrDefault(d => d.DeliveryID == source_id);
DHXEventsHelper.Update(eventToUpdate, changedRecEvent, new List<string> { "id" });
if (eventToUpdate != null && eventToUpdate.RouteID != changedRecEvent.id)
{
var routeToUpdate = context.Routes.SingleOrDefault(d => d.RouteID == changedRecEvent.id);
eventToUpdate.Route = routeToUpdate;
}
context.SubmitChanges();
break;
}
action.TargetId = changedRecEvent.id;
}
}
catch
{
action.Type = DataActionTypes.Error;
}
return (new AjaxSaveResponse(action));
}
The recurring extension (dhtmlxscheduler_recurring.js) doesn't recognize the DHXJson Alias annotations that you are using on your entity class properties (extremely frustrating). Therefore, you must name your entity class columns/properties exactly how the dhtmlxscheduler_recurring.js is expecting them, even though the base scheduler API gives you the option for custom naming using the DHXJson alias annotations.

Categories

Resources