Good day I am new in mongodb, I can successfully perform a CRUD, but now I want to dig deep into mongoDB. I have this JSON and I want it to update my database once it has a data, and create a new one if it doesn't exist, but what happens is it always replace my value for the "LeaderboardDetails".
Here is the MongoDB JSON:
{
"id" : "secretsomething",
"UserID" : "zgahsjd",
"category" : "testing",
"Score" : 2000,
"Badges" : 0,
"LeaderboardDetails": {
"id": "123123123213",
"ScoreForDay": 10000,
"BadgesForDay": 0
}
}
When I submit to update the "LeaderboardDetails" it should add a new entry if it doesn't exist, otherwise update the current one , but instead it replaces it.
Here is my code for PUT:
public void Update(string id, SomeModel newScore)
{
_record.ReplaceOne(scores => scores.id == id, newScore);
}
Here is the SomeModel Code:
[Required]
[JsonProperty(PropertyName = "UserID")]
public string UserID { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string id { get; set; }
[Required]
[JsonProperty(PropertyName = "category")]
public string category { get; set; }
[Required]
public int Score { get; set; }
[Required]
public int Badges { get; set; }
public class LeaderboardIDToSend
{
public string id;
public string ScoreForDay;
public string BadgesForDay;
public LeaderboardIDToSend(string _id, string _score, string _badges)
{
id = _id;
ScoreForDay = _score;
BadgesForDay = _badges;
}
}
[Required]
public LeaderboardIDToSend LeaderboardDetails;
Looking at your json, leaderBoardDetails is an object, but it should be a list of objects, that is the first modification you should do, second, in order for you to add an item to set, you should use Builders<T>.Update.AddToSet(c => c.leaderBoardDetails, leaderboardDto), now regarding your concern, you want to upsert the object, as far as I know, and looking at Array Update Operators, there is no such operator, so you will have to do this manually, you need to load all your existing leader, then check if this id exist, if so, you need to update its values, and then Update the whole list (leaderBoardDetails), if it doesn't exist, you can simple use AddToSet operator
Related
I need to check privileges to specific field in specific object in database.
Let's make and example. I have Model called Employee
public class Employee {
[Key]
public int EmployeeID { get; set; }
public string JobTitle { get; set; }
public string Description { get; set; }
public int Salary { get; set; } // <---- Restricted
public int BossID { get; set; }
}
And I have a few cases:
I need to restrict access to specific field Salary because I don't want anyone to see each other salary. But HR can see anyone Salary and edit it. If I'm this employee I can see my own Salary, but cannot edit it.
Everyone can see each other job titles, but only HR can edit it. And also boss of that employee, can edit, by employee himself cannot.
Use case:
I'm manager with RoleID 4. I want to see Salary of my Employee named John Smith with EmployeeID 5. I can do that.
I'm manager with RoleID 4. I want to see Salary of 'Employeenamed Mark Twain withEmployeeID` 8. Mark is not but my directly subordinate. He is from different branch. I cannot do that.
I'm employee with EmployeeID 5 and I want to see my Salary. That's allowed.
I'm employee with EmployeeID 5 and I want to edit my own Salary. It's forbidden. I get HTTP Error 401.
I'm from HR. I can see and edit Salary of all Employees in company.
I though of something like this:
public class Access {
[Required]
public int RoleID { get; set; }
[Required]
public string TableName { get; set; }
[Required]
public string ColumnName { get; set; }
[Required]
public int RowID { get; set; }
}
And then check (by Authorize attribute) if specific role (boss, HR or something) has access to specific field (for example Salary) for specific data (for example Employee with id 22). That's a lot of "specific"by the way.
How should I do it? Is my idea 'OK'?
In case when logic is less complicated or more generic, it's possible to set custom output formatter to prevent some fields to be written into the respose.
The approach has next problems:
Shouldn't handle complicated logic. As it causes business logic spread to the multiple places
Replaces default serialization. So if there are specific serialization settings are set in Startup, then it should be transfered
Let's see an example.
There could be a custom attrbute like
public class AuthorizePropertyAttribute : Attribute
{
public AuthorizePropertyAttribute(string role) => Role = role;
public string Role { get; set; }
}
Then output formatter could be like:
public class AuthFormatter : TextOutputFormatter
{
public AuthFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
SupportedEncodings.Add(Encoding.UTF8);
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new AuthorizedPropertyContractResolver(context.HttpContext.User)
};
await context.HttpContext.Response.WriteAsync(
JsonConvert.SerializeObject(context.Object, settings));
}
}
That would require
public class AuthorizedPropertyContractResolver : DefaultContractResolver
{
public AuthorizedPropertyContractResolver(ClaimsPrincipal user)
{
User = user;
}
public ClaimsPrincipal User { get; }
protected override JsonProperty CreateProperty(MemberInfo member,
MemberSerialization memberSerialization)
{
var result = base.CreateProperty(member, memberSerialization);
result.ShouldSerialize = e =>
{
var role = member.GetCustomAttribute<AuthorizePropertyAttribute>()?.Role;
return string.IsNullOrWhiteSpace(role) ? true : User.IsInRole(role);
};
return result;
}
}
Registration:
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new AuthFormatter());
});
In that case Response for simple user will lack of the Salary field {"Id":1,"Name":"John"} at the same time manager will see the full response
{"Id":1,"Name":"John","Salary":100000}, ofcourse the property "Salary" should have attribute set
[AuthorizeProperty("Boss")]
public double Salary { get; set; }
You should implement 2 different methods. One is for the HR when requesting the data, and the other is for the simple user. Then you never should return the whole object (json), instead, create some DTOs (Data Transfer Object) that holds the desired data. So lets make an example:
public class DTOGetEmployeeByEmployee {
public int EmployeeID { get; set; }
public string JobTitle { get; set; }
public string Description { get; set; }
public int BossID { get; set; }
}
public class DTOGetEmployeeByHR {
public int EmployeeID { get; set; }
public string JobTitle { get; set; }
public string Description { get; set; }
public int Salary { get; set; }
public int BossID { get; set; }
}
Once a user requests that employee, get it from the Database, and then convert it into the desired DTO. Best way I saw so far is using AutoMapper to do so:
Mapper.Map<DTOxxxx>(yourObject);
You can also use the [Authorize] Attribute to check if the User is HR or an Employee. I did this multiple times combined with JWT-Token.
public class EmployeeController
{
[Authorize("HR")]
[HttpGet, Route("GetForHR")]
public IActionResult Get(int employeeID)
{
// Note: this is just a sample out of my head, so there will be adjustments needed in order to run that
// Check if the HR is allowed to access the Employees data
// Get the Employee by its ID
var emp = ...;
// Convert it to the DTO
var dto = Mapper.Map<DTOGetEmployee>(emp);
// return the dto
return Ok(dto);
}
}
I bet there are plenty of better solutions out there, but for me, this is super simple, wasy to reimplement in other applications and there is no palpable performance loss
I'm not sure how to explain our goal in words, so I'll hop straight to the code.
With our current json converter settings. We get the following result when converting one of our events.
{
"PortfolioId": {
"Id": "portId"
},
"EntityId": {
"Id": "3cf7582b-3cad-4aeb-a671-0132ba97d60d"
},
"EventVersion": 1,
"OccurredOn": "2018-08-08T09:52:03.7871323+02:00",
"Id": "71fe3a2e-354a-4b19-abea-655471e96d72",
"Creator": {
"Id": "27a1d6b1-1ffa-4071-92ee-31c12bf120f0"
},
"CorrelationId": "3138dbe0-3a4d-4559-83e9-d1f3e5684ee8"
}
Our goal is to get a converted event that looks like this;
{
"PortfolioId": "portId",
"EntityId": "3cf7582b-3cad-4aeb-a671-0132ba97d60d",
"EventVersion": 1,
"OccurredOn": "2018-08-08T09:52:03.7871323+02:00",
"Id": "71fe3a2e-354a-4b19-abea-655471e96d72",
"Creator": "27a1d6b1-1ffa-4071-92ee-31c12bf120f0",
"CorrelationId": "3138dbe0-3a4d-4559-83e9-d1f3e5684ee8"
}
In the event we have an object of a certain type (i.e EntityId, PortfolioId) which holds the value in a property. All these Id types derive from an abstract class with the property "Id".
An event class looks like this.
public class ExampleEvent : DomainEvent
{
public PortfolioId PortfolioId { get; }
public EntityId EntityId { get;}
public ExampleEvent(
PortfolioId portfolioId,
EntityId entityId,
UserId creator, Guid correlationId) : base(creator, correlationId)
{
PortfolioId = portfolioId;
EntityId = entityId;
}
}
Does anybody have an idea how one could do this. I thought this might be possible with a custom json converter but have no idea yet on how to implement this for this case.
EDIT: I should have stated that this has to be done one many event types. And that a generic reusable solution seems to fit best in order to keep the overhead low. This mean that it is probably best if the event class itself is not altered at all. (so preferably without attributes etc)
The second Approach in this answer could help in manipulating the serialization.
Making a property deserialize but not serialize with json.net
You can use JsonIgnore attributes with calculated properties:
public class PortfolioId
{
public string Id { get; set; }
}
public class EntityId
{
public string Id { get; set; }
}
public class UserId
{
public string Id { get; set; }
}
public class ExampleEvent
{
private ExampleEvent() // for JSON deserializer
{
Creator = new UserId();
Portfolio = new PortfolioId();
Entity = new EntityId();
}
// add your base constructor call
public ExampleEvent(PortfolioId portfolio, EntityId entity, UserId creator)
{
Creator = creator;
Portfolio = portfolio;
Entity = entity;
}
[JsonIgnore]
public UserId Creator { get; set; }
public string CreatorId
{
get => Creator.Id;
set => Creator.Id = value;
}
[JsonIgnore]
public PortfolioId Portfolio { get; set; }
public string PortfolioId
{
get => Portfolio.Id;
set => Portfolio.Id = value;
}
[JsonIgnore]
public EntityId Entity { get; set; }
public string EntityId
{
get => Entity.Id;
set => Entity.Id = value;
}
public int EventVersion { get; set; }
public string Id { get; set; }
public string CorrelationId { get; set; }
}
Incase JsonIgnore does not suites your need or you need more customization you may also look for IContractResolver with JsonProperty.ShouldDeserialize / JsonProperty.ShouldSerialize. here some examples.
I have the following document called Attendances
{
"_id" : ObjectId("5a4ffb00762caf6b54f61ebb"),
"AttnDate" : ISODate("2018-01-05T22:24:00.490Z"),
"AllAttendances" : [
{
"FullName" : "DOMAIN\Zack",
"Logged" : ISODate("2018-01-05T22:23:46.835Z"),
"Pauses" : [
{
PauseStartAt: ISODate("2018-01-05T22:30:46.835Z"),
PauseEndAt: ISODate("2018-01-05T22:35:46.835Z")
}
]
}
]
}
How can i add new items to Pauses. This is my attempt but i have this error "Cannot convert lambda expression to type 'fielddefinition because it is not a delegate type.
My attempt
var filter = Builders<Attendance>.Filter.Eq(a => a.Id, currentAttn.Id) & Builders<Attendance>.Filter.ElemMatch(s => s.AllAttendances, Builders<TimeRecord>.Filter.Eq(n => n.FullName, userName));
var update = Builders<Attendance>.Update.Push(e => e.AllAttendances[-1].Pauses, pauses);
context.Attendances.FindOneAndUpdate(filter, update);
I followed this guide
Attendance Class
public class Attendance
{
[JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime AttnDate { get; set; }
public List<TimeRecord> AllAttendances { get; set; }
}
TimeRecord Class (AllAttendances)
public class TimeRecord
{
public string FullName { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime Logged { get; set; }
public List<Pause> Pauses { get; set; }
}
Pause Class
public class Pause
{
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime PauseStartedAt { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime PauseEndedAt { get; set; }
}
You need to update your filter to
var filter = Builders<Attendance>.Filter.Eq(a => a.Id, id) &
Builders<Attendance>.Filter.ElemMatch(s => s.AllAttendances, x => x.FullName == userName);
The first argument of ElemMatch is the field, the second argument is the filter.
Looking at it from a different angle, I would suggest you don't use ObjectIDs in c#. I always define ObjectIds as strings in my models and use the Bson attribute decorators to define them as ObjectId's in the database
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
Purely for the pain it causes trying to use ObjectIds in C#. Strings are much easier to handle. Your document in mongodb will still look the same, and you will not need to cast as object id's in your code at all:
_id : ObjectId("xxxxxxx")
This should help you get around the issue of the compiler not knowing how to do the conversion
I have a problem where I create an object containing a list, load it into my database, run a query that returns the object, but find the list null. All other properties of the object are as they should be. I'm using a list called "Ints" that is filled with a few integers but I've tried using other types.
Here's my model:
public class CourseModel
{
public int CourseModelId { get; set; }
public virtual ICollection<int> Ints { get; set; } // the variable in question
public string Name { get; set; }
public string Overview { get; set; }
}
And here's my database population (The database is called LearnYou):
public class LearnYouDbContextInitializer : DropCreateDatabaseAlways<LearnYouDbContext>
{
protected override void Seed(LearnYouDbContext context)
{
context.Courses.Add(new CourseModel()
{
Name = "C# Programming",
Overview = "You'll learn some C#",
Ints = new List<int> { 1, 42, 3 },
});
context.SaveChanges();
}
}
Here's the controller code for querying the object:
// GET: Course/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
CourseModel courseModel = db.Courses.Find(id);
// DEBUGGING THE PREVIOUS LINE SHOWS INTS IS NULL
if (courseModel == null)
{
return HttpNotFound();
}
return View(courseModel);
}
The "Ints" property is not null after saving the context in the database population part but is always null when it's queried (I visit the page ~Edit/1 to debug). I just can't figure out why when all other properties are fine. Any ideas? Thanks.
An ICollection in a model indicates a Parent->Child relationship. However, I doubt EF will be able to determine how to create a child table for an ICollection of integers. Here is what I would do.
Create a new model Ints (or whatever it actually represents):
public class Ints {
public int Value { get; set;}
}
Modify your original model to use it:
public class CourseModel
{
public int CourseModelId { get; set; }
public virtual ICollection<Ints> Ints { get; set; } // See the difference?
public string Name { get; set; }
public string Overview { get; set; }
}
That should make it work.
It Is not working because you are mapping directly to a int primitive type of .net and Entity Framework doesn't allow it.
In this case what you can do is create your onw object for example and sql table like
public class Ints {
{
public Course Course { get; set; }
public int IntValue { ger; set ; }
}
And referencing it from CourseModel
public virtual List<Ints> Ints { get; set; }
This is the code I have:
Controller action method:
[HttpPost]
public ActionResult Create(Goal goal)
{
if (ModelState.IsValid)
{
repository.SaveGoal(goal);
return View("Index");
}
else
{
return View(goal);
}
}
Model:
public class Goal
{
[Required]
public int GoalId { get; set; }
[Required]
public string Name { get; set; }
[Required]
[HiddenInput(DisplayValue = false)]
public decimal Latitude { get; set; }
[Required]
[HiddenInput(DisplayValue = false)]
public decimal Longitude { get; set; }
[Required]
public bool IsPrivate { get; set; }
}
Repository:
public class EFGoalRepository : IGoalRepository
{
private EFDbContext context = new EFDbContext();
public IQueryable<Goal> Goals
{
get { return context.Goals; }
}
public void SaveGoal(Goal goal)
{
context.Goals.Add(goal);
context.SaveChanges(); // the line which causes the exception
}
}
The problem: When I try to save a new Goal object with GoalId set to 0, I get the following error:
Cannot insert the value NULL into column 'GoalId', table 'TravelGoals.dbo.Goals'; column does not allow nulls. INSERT fails.
The statement has been terminated.
I'm still new at ASP.NET MVC, but I believe this worked when I tried it a month ago with a different project.
All the values in my Goal object are valid (non-null, Name is of correct length). The GoalId property is set to 0, so it is not null either. I thought that Entity Framework would automatically assign a value to it.
What can I do to make this work?
What I needed to do was setting the column as identity in SQL Server.
Probably the simplest way to do this (assuming you're using Visual Studio):
Open the SQL Server Object Explorer
Double-click the table you want to edit (or right-click and select View Designer)
Open the Properties window
Select the column in the Identity Column property as shown below
Problem is GoalId is not identity.Put this attribut on GoalId
[Key]
public int GoalId { get; set; }
If your EF version is lower than 5 use this:
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int GoalId { get; set; }