net MVC Web Application. I have a database where i have made my model, i use Linq2SQL to build my Business Logic Layer. In my application i have a customer object, when i call my "editCystomer" page i pass in a Customer to populate the textBoxes:
[AcceptVerbs(HttpVerbs.Get)]
[Authorize]
public ViewResult EditCustomer(string id)
{
int customerId = Convert.ToInt32(id);
CustomerRepository repository = new CustomerRepository();
return View(repository.Load(customerId));
}
When the user has changed in the textboxes i save my changed customer like this:
AcceptVerbs(HttpVerbs.Post)]
[Authorize]
public ActionResult EditCustomer(Customer customer)
{
ValidateCustomer(customer);
if (ModelState.IsValid)
{
CustomerRepository repository = new CustomerRepository();
repository.Save(customer);
return RedirectToAction("CreateCustomerDone");
}
else
{
return View();
}
}
Nothing fancy or unexpected so far, however in my save method:
public void Save(Customer customer)
{
if (customer.Id > 0)
sdc.Refresh(System.Data.Linq.RefreshMode.KeepChanges, customer);
else
sdc.Customers.InsertOnSubmit(customer);
sdc.SubmitChanges();
}
...im getting an exception in my save (the update) that it cannot refresh the object (An object specified for refresh is not recognized. ). I have done this a million times before in other setups, how come its failing now? Any ideas?
The customer object you're sending to Save() is not part of the DataContext. You need to either get the object again, then call the Refresh().
Or you can do the following:
public void Save(Customer customer)
{
if (customer.Id > 0)
{
Customer orig = sdc.Customers.GetOriginalEntityState(customer);
if(orig == null)
sdc.Attach(customer);
sdc.Refresh(System.Data.Linq.RefreshMode.KeepChanges, customer);
}
else
sdc.Customers.InsertOnSubmit(customer);
sdc.SubmitChanges();
}
Related
What is the best practice for saving an entity in wcf. I am calling my service through asp.net mvc site.
I have declared my context in the .svc file, as I would with normal winforms development.
public ScoolEntities database = new ScoolEntities();
Then I am using the following to get the data by id.
public student GetStudentsById(int id)
{
var q = (from mystudent in database.students where mystudent.id == id select mystudent);
return q.ToList()[0];
}
Then Finally I have a public save method
public bool savechanges()
{
database.SaveChanges();
return true;
}
Then in my controller I have
public ActionResult Edit(int id=0)
{
return View(obj.GetStudentsById(id));
}
[HttpPost]
public ActionResult Edit(MvcApplication1.ServiceReference1.student student)
{
if (ModelState.IsValid)
obj.savechanges();
return RedirectToAction("Index");
}
return View();
}
But it does not appear to save the changes and also what do I need to place in the return view I would have thought I call the GetStudents again but it does not appear to work?.
You need to add the Entity to the Context and need to call SaveChanges method.
database.Students.Add(student);
database.SaveChanges();
On POST , if validation failed and before sending back the ViewModel to the same View with Model State errors, do you rebuild ViewModel for all SelectLists, ReadOnly fields etc?
right now I have separate methods for Fill First Time(for GET Edit-Method) / Rebuild ViewModels from domain objects, what is the best practice so I can be DRY and also not have to change two methods any time I add a new readonly property to ViewModel?
My Solution: Followed this Pattern
Followed pattern suggested here: https://stackoverflow.com/a/2775656/57132
In IModelBuilder Implementation
Build(..)
{
var viewModel = new ViewModel();
// and Fill all Non-ReadOnly fields
...
...
call CompleteViewModel(viewModel)
}
CompleteViewModel(ViewModel viewModel)
{
//Fill all ReadOnly & SelectLists
...
}
The reason I went with this solution is because I don't want to store stuff on server to retrieve across the HTTP Requests
I don't rebuild it, because I don't stay at POST. I follow POST-REDIRECT-GET pattern, so if I post to /User/Edit/1 using POST HTTP method, I get redirected to /User/Edit/1 uasing GET.
ModelState is transferred to TempData to follow Post-Redirect-Get and be availabe at GET call. View model is built in one place, at GET call. Example:
[HttpPost]
[ExportModelStateToTempData]
public ActionResult Edit(int id, SomeVM postedModel)
{
if (ModelState.IsValid) {
//do something with postedModel and then go back to list
return RedirectToAction(ControllerActions.List);
}
//return back to edit, because there was an error
return RedirectToAction(ControllerActions.Edit, new { id });
}
[ImportModelStateFromTempData]
public ActionResult Edit(int id)
{
var model = //create model here
return View(ControllerActions.Edit, model);
}
This is code for attributes importing/exporting ModelState:
public abstract class ModelStateTempDataTransferAttribute : ActionFilterAttribute
{
protected static readonly string Key = typeof(ModelStateTempDataTransferAttribute).FullName;
}
public class ExportModelStateToTempDataAttribute : ModelStateTempDataTransferAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//Only export when ModelState is not valid
if (!filterContext.Controller.ViewData.ModelState.IsValid)
{
//Export if we are redirecting
if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
{
filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
}
}
base.OnActionExecuted(filterContext);
}
}
public class ImportModelStateFromTempDataAttribute : ModelStateTempDataTransferAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;
if (modelState != null)
{
//Only Import if we are viewing
if (filterContext.Result is ViewResult)
{
filterContext.Controller.ViewData.ModelState.Merge(modelState);
}
else
{
//Otherwise remove it.
filterContext.Controller.TempData.Remove(Key);
}
}
base.OnActionExecuted(filterContext);
}
}
The simplest solution would be to pass in you viewModel to the method and account for null
private MyViewModel BuildViewModel(MyViewModel model = null)
{
model = model ?? new MyViewModel();
model.ReadOnlyList = new .....
.
.
return model;
}
for Create:
var model = BuildViewModel();
for rebuild:
model = buildViewModel(model);
I like #LukLed's answer above - it looks very interesting. If you want another option, here's what I currently do.
In my service layer, I have a method to build my view model. I call that on GET and return the the view model to the view. On POST, I build the model from the incoming ID and then TryUpdateModel(model). From there, you can do whatever you like (save, check model state, etc.). With this method, you only have 1 build method and only have to update it once if your model changes (i.e. add/remove properties in the future, etc.).
[HttpGet]
public ActionResult AssessFocuses(int apaID)
{
var model = this.apaService.BuildAssessFocusesViewModel(apaID);
return this.View(model);
}
[HttpPost]
public ActionResult AssessFocuses(int apaID, string button)
{
var model = this.apaService.BuildAssessFocusesViewModel(apaID);
this.TryUpdateModel(model);
switch (button)
{
case ButtonSubmitValues.Back:
case ButtonSubmitValues.Next:
case ButtonSubmitValues.Save:
case ButtonSubmitValues.SaveAndClose:
{
try
{
this.apaService.SaveFocusResults(model);
}
catch (ModelStateException<AssessFocusesViewModel> mse)
{
mse.ApplyTo(this.ModelState);
}
if (!this.ModelState.IsValid)
{
this.ShowErrorMessage(Resources.ErrorMsg_WEB_ValidationSummaryTitle);
return this.View(model);
}
break;
}
default:
throw new InvalidOperationException(string.Format(Resources.ErrorMsg_WEB_InvalidButton, button));
}
switch (button)
{
case ButtonSubmitValues.Back:
return this.RedirectToActionFor<APAController>(c => c.EnterRecommendationsPartner(model.ApaID));
case ButtonSubmitValues.Next:
return this.RedirectToActionFor<APAController>(c => c.AssessCompetenciesPartner(model.ApaID));
case ButtonSubmitValues.Save:
this.ShowSuccessMessage(Resources.Msg_WEB_NotifyBarSuccessGeneral);
return this.RedirectToActionFor<APAController>(c => c.AssessFocuses(model.ApaID));
case ButtonSubmitValues.SaveAndClose:
default:
return this.RedirectToActionFor<UtilityController>(c => c.CloseWindow());
}
}
I am designing an MVC 3 application where multiple tenants reside in a single database.
What is the best way to prevent users from editing/viewing other tenants data in MVC? (i.e. someone could type in '/People/Edit/1' and edit the person with Id of 1- regardless of wether they are part of the tenants data or not).
I know I can override 'OnActionExecuting(ActionExecutingContext filterContext)' for each controller- but it sounds crazy to have to handle each action seperately, get the ID or OBJECT depending on if its a POST or GET and then check if the operation is allowed.
Any better ideas?
Also, I do not want to go down the route of creating a different database or schema for each tenant.
Thanks in advance.
Instead of passing ids to your controller actions write a custom model binder for your entities which will fetch it from the database. So for example let's assume that you have the following model:
public class Person
{
public string Id { get; set; }
... some other properties
}
Now instead of having:
[HttpPost]
public ActionResult Edit(string id)
{
...
}
write:
[HttpPost]
public ActionResult Edit(Person person)
{
...
}
and then write a custom model binder for Person:
public class PersonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var id = bindingContext.ValueProvider.GetValue("id");
// check if an id was provided and if the user is authenticated
if (!controllerContext.HttpContext.User.Identity.IsAuthenticated || id == null)
{
throw new HttpException(403, "Forbidden");
}
var currentUser = controllerContext.HttpContext.User.Identity.Name;
// fetch the person from your repository given the id and belonging
// to the currently authenticated user
var person = _repository.GetPerson(id.AttemptedValue, currentUser);
if (person == null)
{
// no person found matching
throw new HttpException(403, "Forbidden");
}
return person;
}
}
which you would register in Application_Start:
ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
Original answer
To solve the problem quickly use guids instead of a auto-increasing integer. However this is just delaying the problem.
One of the things you can do is to role your own authorize attribuut http://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute.aspx Or you can chose to create a global actionfilter. http://www.asp.net/mvc/tutorials/understanding-action-filters-cs
Addition information on how you could do it based on request in comment
public class MySuperFilter : ActionFilterAttribute
{
//Called by the MVC framework before the action method executes.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
String user = filterContext.HttpContext.User.Identity.Name;
int id = int.Parse(filterContext.RouteData.GetRequiredString("Id"));
if (!IsValidUser(user,id))
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary {{ "Controller", "YourController" },
{ "Action", "YourAction" } });
}
base.OnActionExecuting(filterContext);
}
private bool IsValidUser(string user,int id)
{
//Check if the user has acces to the page
return true;
}
}
It's a non-elegant solution but depending on scope it could be easiest. Having designed a similar system that has relatively few points of entry for multiple tenants data we just merely check that the CurrentUser is the Owner of the object that is being queried. Our use objects a common base interface that has the owner field so the check is made not carrying about the specific object but just from the interface. If there's a mismatch we throw a security exception and log that a user is probably playing around the query string seeing if they get our website to leak data the way many production websites do.
I have been tearing my hair out over this for days and before I go completely bald it's time to ask all the people smarter than me how to do this.
I am using Entity Framework 4 with the Code First CTP 5 and MVC 3.
The exception message right now is "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key."
First up here is the controller the edit form is posted to:
public ActionResult Save(ClientEntity postedClient)
{
try
{
if (ModelState.IsValid)
{
Base.clientInterface.Save(postedClient);
return RedirectToAction("Index");
}
}
catch (Exception)
{
throw;
}
SetupManageData(null, postedClient);
return View("Manage");
}
The Save method on the client interface is this:
public void Save(ClientEntity theClient)
{
SetContext();
if (theClient.clientId == 0)
this.pContext.Clients.Add(theClient);
else
{
ClientEntity existingClient = GetSingle(theClient.clientId); // Get the existing entity from the data store.
// PseudoCode: Merge existingClient and theClient - Can this be done without using ObjectStateManager?
// PseudoCode: Attach merged entity to context so that SaveChanges will update it in the database - is this correct?
}
this.pContext.SaveChanges();
}
private void SetContext()
{
if (this.pContext == null)
this.pContext = new PersistanceContext();
}
Persistance context is the DBContext and looks like this:
public class PersistanceContext : DbContext
{
public DbSet<ClientEntity> Clients { get; set; }
}
What is the lifestyle of clientInterface? is it a singleton or something that keeps it alive across multiple requests?
My guess is that it holds a live instance of a database context that was used to fetch the entity in the GET request and when the POST tries to (re)add a client entity to the context the old one is still there and they conflict.
Try destroying the object that is behind the clientInterface with every request. Maybe use a DI container that supports per-webrequest lifestyles so you don't have to worry about it.
I hope my guess was right and this is of help.
This should work.
if (theClient.clientId == 0)
{
this.pContext.Clients.Add(theClient);
}
else
{
ClientEntity existingClient = this.pContext.Clients.Single(o => o.ClientId == theClient.ClientId);
// map properties
existingClient.Name = theClient.name;
// ....
}
this.pContext.SaveChanges();
[Edit]
It's easier (IMHO) to split the creation & editing of objects into 2 seperate views and to avoid the mapping of the properties I use TryUpdateModel.
[HttpPost]
public ViewResult Edit(int clientID, FormCollection collection)
{
var client = pContext.Clients.SingleOrDefault(o => o.ID == clientID);
if(!TryUpdateModel(client, collection))
{
ViewBag.UpdateError = "Update Failure";
}
else
{
db.SubmitChanges();
}
return View("Details", client);
}
[HttpPost]
public ViewResult Create(FormCollection collection)
{
var client = new Client();
if(!TryUpdateModel(client, collection))
{
ViewBag.UpdateError = "Create Failure";
}
else
{
db.Clients.Add(client);
db.SubmitChanges();
}
return View("Details", client);
}
I have a ProductController with actions Index (which Loads a blank form). The form also posts to itself as its a complex form and the form elements like dropdowns show posted values
the code is as follows
public ActionResult Index()
{
int id;
id = Convert.ToInt32(Request.Form["ddlLendingType"]);
if (id == 0)
id = 1;
ProductCommonViewModel viewData = new ProductCommonViewModel(_prodRepository.Method1(),_prodRepository.Method2())
return View(viewData);
}
When I click submit from the form, it saves the product and if it fails it should show the validation error messages.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Save(FormCollection fc)
{
Product product = new Product();
try
{
...fill out all properties from form collection
_prodRepository.SaveProduct(product);
return RedirectToAction("Index", "Product");
}
catch (Exception ex)
{
TempData["Message"] = "An Error Occured while saving the product!";
Validation.UpdateModelStateWithRuleViolation(product, ViewData.ModelState);
// WHEN I call redirect to action Index on this view I can see the TempData variable but I cannot see validation summary and individual validation messages.How do I persist the msgs across requests?
}
}
The helper method definition is as follows:
public static void UpdateModelStateWithRuleViolation(IRuleEntity entity, ModelStateDictionary dictModel)
{
List<RuleViolation> violations = entity.GetRuleViolations();
foreach (var item in violations)
{
dictModel.AddModelError(item.PropertyName, item.ErrorMessage);
}
}
Pass modelstate into tempdata too.
Btw, instead of this:
public ActionResult Index()
{
int id; //and here You could join declaration with assignment
id = Convert.ToInt32(Request.Form["ddlLendingType"]);
You can do this:
public ActionResult Index(int ddlLendingType)
{
And using FormCollection is a bad practice which should not be used. For extreme cases - create custom model binder (CodeCampServer has quite nice binding mechanism) or action filter (Kigg`s source).
I had a problem with preserving TempData across multiple requests, I did the following, to refresh the TempData for every redirect action:
protected override RedirectToRouteResult RedirectToAction(string actionName,
string controllerName, System.Web.Routing.RouteValueDictionary routeValues)
{
TempData["Notice"] = TempData["Notice"];
TempData["Error"] = TempData["Error"];
TempData["Warning"] = TempData["Warning"];
return base.RedirectToAction(actionName, controllerName, routeValues);
}