So I just fixed a bug in a framework I'm developing. The pseudo-pseudocode looks like this:
myoldObject = new MyObject { someValue = "old value" };
cache.Insert("myObjectKey", myoldObject);
myNewObject = cache.Get("myObjectKey");
myNewObject.someValue = "new value";
if(myObject.someValue != cache.Get("myObjectKey").someValue)
myObject.SaveToDatabase();
So, essentially, I was getting an object from the cache, and then later on comparing the original object to the cached object to see if I need to save it to the database in case it's changed. The problem arose because the original object is a reference...so changing someValue also changed the referenced cached object, so it'd never save back to the database. I fixed it by cloning the object off of the cached version, severing the reference and allowing me to compare the new object against the cached one.
My question is: is there a better way to do this, some pattern, that you could recommend? I can't be the only person that's done this before :)
Dirty tracking is the normal way to handle this, I think. Something like:
class MyObject {
public string SomeValue {
get { return _someValue; }
set {
if (value != SomeValue) {
IsDirty = true;
_someValue = value;
}
}
public bool IsDirty {
get;
private set;
}
void SaveToDatabase() {
base.SaveToDatabase();
IsDirty = false;
}
}
myoldObject = new MyObject { someValue = "old value" };
cache.Insert("myObjectKey", myoldObject);
myNewObject = cache.Get("myObjectKey");
myNewObject.someValue = "new value";
if(myNewObject.IsDirty)
myNewObject.SaveToDatabase();
I've done similar things, but I got around it by cloning too. The difference is that I had the cache do the cloning. When you put an object into the cache, the cache will clone the object first and store the cloned version (so you can mutate the original object without poisoning the cache). When you get an object from the cache, the cache returns a clone of the object instead of the stored object (again so that the caller can mutate the object without effecting the cached/canonical object).
I think that this is perfectly acceptable as long as the data you're storing/duping is small.
A little improvement on Marks anwser when using linq:
When using Linq, fetching entities from DB will mark every object as IsDirty.
I made a workaround for this, by not setting IsDirty when the value is not set; for this instance: when null. For ints, I sat the orig-value to -1, and then checked for that. This will not work, however, if the saved value is the same as the uninitialized value (null in my example).
private string _name;
[Column]
public string Name
{
get { return _name; }
set
{
if (value != _name)
{
if (_name != null)
{
IsDirty = true;
}
_name = value;
}
}
}
Could probably be improved further by setting IsDirty after initialization somehow.
Related
I have dotnet WebAPI and I'm trying to get a specific behaviour but am constantly getting 415 responses.
I have reproduced this by starting a new webapi project using dotnet new webapi on the command line. From there, I added two things: a new controller, and a model class. In my real project the model class is obviously a bit more complex, with inheritance and methods etc...
Here they are:
[HttpGet("/data")]
public async Task<IActionResult> GetModel(BodyParams input)
{
var response = new { Message = "Hello", value = input.valueOne };
return Ok(response);
}
public class BodyParams {
public bool valueOne { get; set; } = true;
}
My goal is that the user can call https://localhost:7222/data with no headers or body needed at all, and will get the response - BodyParams will be used with the default value of true. Currently, from postman, or from the browser, I get a 415 response.
I've worked through several suggestions on stack and git but nothing seems to be working for me. Specifically, I have tried:
Adding [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] into the controller, but this makes no difference unless I provide an empty {} json object in the body. This is not what I want.
Making BodyParams nullable - again, no change.
Adding .AddControllers(opt => opt.AllowEmptyInputInBodyModelBinding = true)... again, no change.
I Implemented the solution suggested here using the attribute modification in the comment by #HappyGoLucky. Again, this did not give the desired outcome, but it did change the response to : 400 - "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true."
I tried modifying the solution in (4) to manually set context.HttpContext.Request.Body to an empty json object... but I can't figure out the syntax for this because it need to be a byte array and at that point I feel like I am way over complicating this.
How can I get the controller to use BodyParams with default values in the case that the user provides no body and no headers at all?
You can achieve that using a Minimal API.
app.MapGet("/data",
async (HttpRequest httpRequest) =>
{
var value = true;
if (Equals(httpRequest.GetTypedHeaders().ContentType, MediaTypeHeaderValue.Parse("application/json")))
{
var bodyParams = await httpRequest.ReadFromJsonAsync<BodyParams>();
if (bodyParams is not null) value = bodyParams.ValueOne;
}
var response = new {Message = "Hello", value};
return Results.Ok(response);
});
So, as there doesn't seem to be a more straightforward answer, I have currently gone with the approach number 5) from the OP, and just tweaking the code from there very slightly.
All this does is act as an action which checks the if the user has passed in any body json. If not, then it adds in an empty anonymous type. The behaviour then is to use the default True value from the BodyParams class.
The full code for the action class is:
internal class AllowMissingContentTypeForEmptyBodyConvention : Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
action.Filters.Add(new AllowMissingContentTypeForEmptyBodyFilter());
}
private class AllowMissingContentTypeForEmptyBodyFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (!context.HttpContext.Request.HasJsonContentType()
&& (context.HttpContext.Request.ContentLength == default
|| context.HttpContext.Request.ContentLength == 0))
{
context.HttpContext.Request.ContentType = "application/json";
var str = new { };
//convert string to jsontype
var json = JsonConvert.SerializeObject(str);
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
context.HttpContext.Request.Body = new MemoryStream(requestData);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
// Do nothing
}
}
}
Then you can add this to any of your controllers using [AllowMissingContentTypeForEmptyBodyConvention]
Recently the IFormCollection in the platform I'm building started returning values of the type Microsoft.Extensions.Primitives.StringValues. when it used to return strings.
The controllers were made with strings in mind and now that are a lot of forms that are not working.
Is there any explanation to this, or a way to revert it?
As far as I'm aware ASP.NET Core's IFormCollection has always been a collection of StringValues. The reason is simple: multiple values can be posted for any particular key, making it potentially impossible to set the value if the type was merely string. There is no way to "revert" this. Change your code accordingly.
Or, better yet, stop using IFormCollection. Bind to strongly-typed models. That's always the best way.
For others coming here also confused by seeing IFormCollection giving StringValues. You may be familiar with .NET Frameworks FormCollection class, which gives strings. The reason for the change is valid and explained by #Chris Pratt in his answer here.
To make IFormCollection and StringValues feel familiar again consider any of these simple extensions:
// Example: var name = collection["name"].FirstOrNull();
// Equal to (.NET Framework): var name = collection["name"];
public static string FirstOrNull(this StringValues values)
{
if (values.Count > 0)
{
return values[0];
}
return null;
}
// Example: var name = collection["name"].FirstOr("John Doe");
// Equal to (.NET Framework): var name = collection["name"] ?? "John Doe";
public static string FirstOr(this StringValues values, string fallback)
{
if (values.Count > 0)
{
return values[0];
}
return fallback;
}
// Example: var name = collection.ValueOrFallback("name", "John Doe");
// Equal to (.NET Framework): var name = collection["name"] ?? "John Doe";
public static string ValueOrFallback(this IFormCollection collection, string key, string fallback)
{
if (collection[key].Count > 0)
{
return collection[key][0];
}
return fallback;
}
Also consider the built-in TryGetValue:
if (collection.TryGetValue("name", out var name))
{
// at least one name did exist
}
Alt.
var name = collection.TryGetValue("name", out var names) ? names[0] : "John Doe";
Is there any method for storing global variables without using cookies or session[""] in asp.net mvc ?
I know that cookies and session[""] have some disadvantages and I want to use the best method if exit.
If they are indeed global variables, you should implement the singleton pattern and have an Instance globally accessible that holds your variables.
Here is a simple example:
public sealed class Settings
{
private static Settings instance = null;
static readonly object padlock = new object();
// initialize your variables here. You can read from database for example
Settings()
{
this.prop1 = "prop1";
this.prop2 = "prop2";
}
public static Settings Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Settings();
}
return instance;
}
}
}
// declare your global variables here
public string prop1 { get; set; }
public string prop2 { get; set; }
}
The you can use them in your code like this:
var globalvar1 = Settings.Instance.prop1;
This class with its variables will be initialized only once (when the application starts) and it will be available in your application globally.
Basically you have following options:
Cookies - valid as long as you set, must be allowed by client's browser, can be deleted by user, stored on user's PC.
Session - valid for all requests, not for a single redirect, stored on server.
ViewData - after redirect it's cleared (lives only during single request).
TempData - it's useful for passing short messages to view, after reading a value it's deleted.
ViewBag - is available only during the current request, if redirection occurs then it’s value becomes null, is dynamic so you don't have intellisense and errors may occur only in runtime.
Here - http://www.dotnet-tricks.com/Tutorial/mvc/9KHW190712-ViewData-vs-ViewBag-vs-TempData-vs-Session.html - you can find fantastic article which describes them.
Sure: HttpContextBase.Application (no expiration) or HttpContextBase.Cache (with expiration). You can access the HttpContextBase instance through the HttpContext property of the Controller class.
So... HACK ALERT... There is no good way to do an MVC 5 or 6 web app using session variables that I have found (yet). MVC doesn't support Session variables or Cookies, which are implemented via session variables. Global variables will be set for ALL users, which is not how Session variables work.
However, you can store "session variables" based on the User.Identity.Name or the underlying User.Identity.Claims.AspNet.Identity.SecurityStamp into a database along with a timestamp and viola! You have implemented primitive session variables. I had a very specific need to save two weeks of programming by not interfering with the GUI that our user interface specialist had written. So I returned NoContent() instead of the normal View() and I saved my hacky session variable based on the user's login name.
Am I recommending this for most situations? No. You can use ViewBag or return View(model) and it will work just fine. But if you need to save session variables in MVC for whatever reason, this code works. The code below is in production and works.
To retrieve the data...
string GUID = merchdata.GetGUIDbyIdentityName(User.Identity.Name);
internal string GetGUIDbyIdentityName(string name)
{
string retval = string.Empty;
try
{
using (var con = new SqlConnection(Common.DB_CONNECTION_STRING_BOARDING))
{
con.Open();
using (var command = new SqlCommand("select GUID from SessionVariablesByIdentityName md where md.IdentityName = '" + name + "' and LastSaved > getdate() - 1", con))
{
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
retval = reader["GUID"].ToString();
}
}
}
}
}
catch (Exception ex)
{
}
return retval;
}
To save the data...
merchdata.SetGUIDbyIdentityName(User.Identity.Name, returnedGUID);
internal void SetGUIDbyIdentityName(string name, string returnedGUID)
{
RunSQL("exec CRUDSessionVariablesByIdentityName #GUID='" + returnedGUID + "', #IdentityName = '" + name + "'");
}
internal void RunParameterizedSQL(SqlConnection cn, SqlCommand cmd, object sqlStr)
{
string retval = string.Empty;
try
{
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
BTW: The SQL table (named SessionVariablesByIdentityName here) is fairly straightforward and can store lots of other things too. I have a LastSaved datetime field in there so I don't bother retrieving old data from yesterday. For example.
I'm trying to do something which should be very simple...I have a site with a dropdown from which the user selects a group. Thereafter, the user navigates through the site using querystring arguments from menus. So I want the caching to be dependent on the querystring - this seems to work. I also want the cache to be dependent on the group that they selected.
But when the querystring is empty, neither cache element seems to work - the page is just whatever the version was for the last selected group. My cache directive looks like this:
<%# OutputCache Duration="300" VaryByCustom="currentAtomId" VaryByParam="documentId;folderId;sectionId;renderMode;typeId" %>
My varyByCustom code looks like this:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
switch (custom)
{
case "currentAtomId":
var currentAtomId = SecurityManifold.Create().CurrentAtomId;
var returnString = currentAtomId == null ? Guid.NewGuid().ToString() : currentAtomId.ToString();
return returnString;
default:
throw new ArgumentException(string.Format("Argument '{0}' is not a valid cache argument.", custom));
}
}
The call to CurrentAtomId boils down to this:
public static int? GetCurrentAtomIdFromContext(HttpContext context)
{
int entityId;
if (context.Session == null)
{
throw new InvalidOperationException("Session is null");
}
var sessionEntityId = context.Session["CurrentEntityId"];
if (sessionEntityId == null || string.IsNullOrEmpty(sessionEntityId.ToString()))
{
return null;
}
if (!int.TryParse(sessionEntityId.ToString(), out entityId))
{
return null;
}
return entityId;
}
Finally, the code which specifies the CurrentEntityId is this:
var selectedEntityId = this.lstSecurityEntities.SelectedValue;
if (string.IsNullOrEmpty(selectedEntityId))
{
return;
}
Session["CurrentEntityId"] = selectedEntityId;
var possibleQueryString = Request.QueryString.ToString();
if (!string.IsNullOrEmpty(possibleQueryString))
{
possibleQueryString = "?" + possibleQueryString;
}
Response.Redirect("default.aspx" + possibleQueryString);
I'm baffled. Any thoughts would be appreciated.
I eventually determined the problem - when output caching is placed at a PAGE level (as opposed to a control level), the session is not available, and throws an exception. Because this exception is occurring in Global ABOVE the global error handler, it fails silently. I eventually figured this out by wrapping a try-catch block around the cache key generation code in VaryByCustomString and Response.Write-ing it out.
What a beatdown...at any rate, the solution is to implement caching at the control level, which unfortunately is a lot more work because the pieces of the page work together...but it's better than no caching. I hope this helps save somebody else some time.
Bottom Line: for varyByCustomString in global.asax - SESSION IS NOT AVAILABLE WHEN CACHING AT THE PAGE LEVEL.
I am trying to switch from saving session data "InProc" to "StateServer".
To do that, I have marked a bunch of classes as [Serializable] and rewritten some classes that could not be serialized before, and marked some values that should not be serialized as [NonSerialized].
Now my problem is that instead of getting a compile-time error, an exception or any other indication of a problem from the framework, I get sessions back where some of the values stored are changed to null-values, either in the session itself or inside the objects contained in the session.
Why is there no indication of an error?
What causes the null-values?
How can I detect that the serialization of the session was correct?
Thanks
Sounds like you need some unit tests to confirm your serialization is working correctly.
[Serializable]
public class SomeClass {
public string SomeValue1;
public string SomeValue2;
}
class Program {
static void Main(string[] args) {
var value1 = new SomeClass() { SomeValue1 = "Hello", SomeValue2 = "World" };
var ms = new MemoryStream();
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
formatter.Serialize(ms, value1);
ms.Position = 0;
var value2 = (SomeClass)formatter.Deserialize(ms);
Debug.Assert(value1.SomeValue1 == value2.SomeValue1);
Debug.Assert(value1.SomeValue2 == value2.SomeValue2);
}
}