TryUpdateModel causing error from unit test cases (Asp.net mvc) - asp.net-mvc-3

I have on post action in controller. Code is as given below
[HttpPost]
public ActionResult Create(Int64 id, FormCollection collection)
{
var data = Helper.CreateEmptyApplicationsModel();
if (TryUpdateModel(data))
{
// TODO: Save data
return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
}
else
{
// TODO: update of the model has failed, look at the error and pass back to the view
if (!ModelState.IsValid)
{
if (id != 0) Helper.ShowLeftColumn(data, id);
return View("Create", data);
}
}
return RedirectToAction("Details", "Info", new { area = "Deals", InfoId= id });
}
I wrote test case for this as below
[TestMethod]
public void CreateTest_for_post_data()
{
var collection = GetApplicantDataOnly();
_controller.ValueProvider = collection.ToValueProvider();
var actual = _controller.Create(0, collection);
Assert.IsInstanceOfType(actual, typeof(RedirectToRouteResult));
}
When I debug this single test case, test case passed because condition
if (TryUpdateModel(data)) return true and its goes to if condition.
But when I debug test cases from entire solution this test case failed because it goes to else condition of " if (TryUpdateModel(data))".
I dont know why..
Please help...
Thanks

I've experienced a similar problem which will solve your issue provided you don't need to use a FormCollection.
I haven't used TryUpdateModel ever since the day I learned of the Auto-Binding feature. Auto-binding, in a nutshell pretty much does the work TryUpdateModel would do for you, that is, it'll set a model object according to the values found in the FormCollection as well as attempting to validate the model. And it does this automatically. All you'll have to do is place a parameter in the ActionMethod and it will automatically have it's properties filled with the values found in the FormCollection. Your Action signature will then turn into this:
public ActionResult Create(Int64 id, SomeModel data)
Now you don't need to call TryUpdateModel at all. You still need to check if the ModelState is valid to decide whether or not to redirect or return a view.
[HttpPost]
public ActionResult Create(Int64 id, SomeModel data)
{
if (ModelState.IsValid)
{
// TODO: Save data
return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
}
else
{
if (id != 0) Helper.ShowLeftColumn(data, id);
return View("Create", data);
}
}
This won't throw an exception in your unit tests so that's one problem solved. However, there's another problem now. If you run your application using the above code, it'll work just fine. You model will be validated upon entering the Action and the correct ActionResult, either a redirect or a view, will be returned. However, when you try to unit test both paths, you'll find the model will always return the redirect, even when the model is invalid.
The problem is that when unit testing, the model isn't being validated at all. And since the ModelState is by default valid, ModelState.IsValid will always return true in your unit tests and thus will always return a redirect even when the model is invalid.
The solution: call TryValidateModel instead of ModelState.IsValid. This will force your unit test to validate the model. One problem with this approach is that means the model will be validated once in your unit tests and twice in your application. Which means any errors discovered will be recorded twice in your application. Which means if you use the ValidationSummary helper method in your view, your gonna see some duplicated messages listed.
If this is too much to bear, you can clear the ModelState before calling TryValidateModel. There are some problems with doing so because your gonna lose some useful data, such as the attempted value if you clear the ModelState so you could instead just clear the errors recorded in the ModelState. You can do so by digging deep into the ModelState and clearing every error stored in every item like so:
protected void ClearModelStateErrors()
{
foreach (var modelState in ModelState.Values)
modelState.Errors.Clear();
}
I've placed the code in a method so it can be reused by all Actions. I also added the protected keyword to hint that this might be a useful method to place in a BaseController that all your controllers derive from so that they all have access to this method.
Final Solution:
[HttpPost]
public ActionResult Create(Int64 id, SomeModel data)
{
ClearModelStateErrors();
if (ModelState.IsValid)
{
// TODO: Save data
return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
}
else
{
if (id != 0) Helper.ShowLeftColumn(data, id);
return View("Create", data);
}
}
NOTE: I realize I didn't shed any light on the root issue. That's because I don't completely understand the root issue. If you notice the failing unit test, it fails because a ArgumentNullException was thrown because the ControllerContext is null and it is passed to a method which throws an exception if the ControllerContext is null. (Curse the MVC team with their damned defensive programming).
If you attempt to mock the ControllerContext, you'll still get an exception, this time a NullReferenceException. Funnily enough, the stack trace for the exception shows that both exceptions occur at the same method, or should I say constructor, located at System.Web.Mvc.ChildActionValueProvider. I don't have a copy of the source code handy so I have no idea what is causing the exception and I've yet to find a better solution than the one I offered above. I personally don't like my solution because I'm changing the way I code my application for the benefit of my unit tests but there doesn't seem to be a better alternative. I bet the real solution will involve mocking some other object but I just don't know what.
Also, before anyone gets any smart ideas, mocking the ValueProvider is not a solution. It'll stop exceptions but your unit tests won't be validating your model and your ModelState will always report that the model is valid even when it isn't.

You might want to clean up your code a bit:
[HttpPost]
public ActionResult Create(int id, FormCollection collection)
{
var data = Helper.CreateEmptyApplicationsModel();
if (!ModelState.IsValid)
{
if (id != 0)
{
Helper.ShowLeftColumn(data, id);
}
return View("Create", data);
}
if (TryUpdateModel(data))
{
return RedirectToAction("Edit", "Applications", new { area = "Applications", id = id });
}
return RedirectToAction("Details", "Info", new { area = "Deals", InfoId= id });
}
Don't use Int64, just use int.
As for your failing test, I would expect your test to fail all the time since TryUpdateModel will return false. As your running the code from a unit test, the controller context for a controller is not available, thus TryUpdateModel will fail.
You need to somehow fake/mock TryUpdateModel so that it does not actually run properly. Instead you "fake" it to return true. Here's some links to help:
How do I Unit Test Actions without Mocking that use UpdateModel?
The above SO answer shows an example using RhinoMocks which is a free mocking framework.
Or this:
http://www.codecapers.com/post/ASPNET-MVC-Unit-Testing-UpdateModel-and-TryUpdateModel.aspx

Debug your tests and check the modelstate errors collection, all the errors that the tryupdatemodel encountered should be there.

Related

ASP.NET MVC Partial View Post-Back, Inconsistent Updating and Bizzare Behavior

I'm developing an ASP.Net MVC application, and am running into a bizzare issue when trying to update data in my database via a partial postback. I'm still new when it comes to HTTP, AJAX, etc., so I'm hoping it's an obvious error.
Basically, when I try to update a table linking content areas to assessments, the update sometimes works, sometimes doesn't. What's bizzare is that after I post, I query the database directly from the MVC application just to make sure that the expected change was in fact made (that's what all the ViewBag.DebugInfo's are doing in the code below). In every case, the query returns what I hope to see. But then when I query the table via SSMS, I see that the change only sometimes goes through.
How is it that a direct query to a table from my MVC application is showing that an update went through, while I can clearly see that it didn't via SSMS? Is there a silent rollback or something? This is maddening and any help would be much appreciated.
A few bits of info:
When I run the "saveTheData" function from the assessmentContent class below outside of MVC, it is always successful.
The update is always successful on the first post.
The update is successful only about half the time on subsequent posts.
When the update is not successful, the the direct query checks from the MVC application nevertheless do seem to show that the update made it all the way to the table.
I can barely make out a pattern in the failures. Namely, it seems that whenever I try to update to a higher contentId value, it is successful, if I try to update to a lower contentId, it is not. For instance, updating from a value of 1 (Math) to 2 (Reading) will always go through, but the reverse will not. This pattern does not manifest if it's the first post from the parent view, or if it's updated via Linqpad.
I put insert, update, and delete triggers on the database table that write to a logging table to see if perhaps the changes were being rolled back. But no entries on the log table on failure. But I also don't know if rollbacks would undo the triggers as well.
I queried dbo.fn_dblog() filtered for Operation = 'LOP_ABORT_XACT', but nothing (though I don't have trained eyes for that difficult view).
Here's my class that fetches and updates the data:
public class assessmentContent {
public int? assessmentId { get; set; }
public List<short> baseline { get; set; } = new List<short>();
public List<short> comparison { get; set; } = new List<short>();
public assessmentContent() { if (assessmentId != null) refreshTheData(); }
public assessmentContent(int assessmentId) {
this.assessmentId = assessmentId;
refreshTheData();
}
public void saveTheData() {
List<short> upserts = comparison.Except(baseline).ToList();
List<short> deletes = baseline.Except(comparison).ToList();
foreach (var upsert in upserts)
reval.ach.addAssessmentContent(assessmentId, upsert);
foreach (var delete in deletes)
reval.ach.deleteAssessmentContent(assessmentId, delete);
refreshTheData();
}
void refreshTheData() {
baseline = reval.ach.assessmentContent(assessmentId).ToList();
comparison = reval.ach.assessmentContent(assessmentId).ToList();
}
}
The logic works fine when I use it outside of my MVC application. So, for instance, if I use it via linqpad, there are no issues. I should mention that assessmentContent() could be named 'getAssessmentContent()'.
Here's my Controller for the Partial View, and some related Code:
public class ContentsModel {
public int? assessmentId { get; set; }
public List<short> comparison { get; set; }
}
public class ContentsController : Controller {
public static string nl = System.Environment.NewLine;
public ActionResult ContentsView(int assessmentId) {
ViewBag.DebugInfo = new List<string>();
var vm = new ContentsModel();
vm.assessmentId = assessmentId;
vm.comparison = reval.ach.assessmentContent(assessmentId).ToList();
return View("~/Views/ach/Contents/ContentsView.cshtml", vm);
}
public ActionResult update(ContentsModel vm) {
ViewBag.DebugInfo = new List<string>();
sqlFetch();
ViewBag.DebugInfo.Add($"VM Pased In {vm.assessmentId} c{vm.comparison.intsJoin()}");
sqlFetch();
var crud = new crud.ach.assessmentContent((int)vm.assessmentId);
ViewBag.DebugInfo.Add($"newly fetched CRUD {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();
crud.comparison = vm.comparison;
ViewBag.DebugInfo.Add($"CRUD after crud_comparison = vm_comparison {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();
crud.saveTheData();
ViewBag.DebugInfo.Add($"CRUD after save {crud.assessmentId} b{crud.baseline.intsJoin()} c{crud.comparison.intsJoin()}");
sqlFetch();
vm.comparison = crud.comparison;
ViewBag.DebugInfo.Add($"VM after vm_comparison = crud_comparison {vm.assessmentId} c{vm.comparison.intsJoin()}");
sqlFetch();
return PartialView("~/Views/ach/Contents/ContentsView.cshtml", vm);
}
void sqlFetch() {
ViewBag.DebugInfo.Add(
"SQL Fetch " +
Sql.ExecuteOneColumn<short>("select contentId from ach.assessmentContent where assessmentId = 12", connections.research).intsJoin()
);
}
}
public static partial class extensions {
public static string intsJoin(this IEnumerable<short> ints) {
var strings = new List<string>();
foreach (int i in ints)
strings.Add(i.ToString());
return string.Join(",", strings);
}
}
I'm aware that I might not have the 3-tier architecture or the Model-View-Controller structure best implemented here.
You'll notice that, in my desperation, I put in a direct check to the database table at every point of change in models.
The Partial View:
#model reval.Views.ach.Contents.ContentsModel
#using reval
#{Layout = "";}
<div id="contentDiv">
<form id="contentForm">
#Html.HiddenFor(m => m.assessmentId)
#Html.ListBoxFor(
m => m.comparison,
new reval.ach.content()
.GetEnumInfo()
.toMultiSelectList(
v => v.Value,
d => d.DisplayName ?? d.Description ?? d.Name,
s => Model.comparison.Contains((short)s.Value)
),
new { id = "contentListBox" }
)
</form>
<br/>
#foreach(string di in ViewBag.DebugInfo) {
#Html.Label(di)
<br/>
}
</div>
<script>
$("#contentListBox").change(function () {
$.ajax({
url: "/Contents/update",
type: "get",
data: $("#contentForm").serialize(),
success: function (result) {
$("#contentDiv").html(result);
},
error: function (request, status, error) {
var wnd = window.open("about:blank", "", "_blank");
wnd.document.write(request.responseText);
}
});
})
</script>
And Finally, the call from the main View:
<div id="testDiv">
#if (Model.assessment != null && Model.assessment.assessmentId != null) {
Html.RenderAction("ContentsView", "Contents", new { assessmentId = Model.assessment.assessmentId });
}
</div>
Are you sure that the transaction you are making to the database is committed or finalized? There could be other transactions going on at same time that could roll back the ones you are making.
I hate it when I solve an issue without exactly pinning down what I did to solve it. But after working on some of the code above, I got it working correctly and consistently.
I'm almost certain, however, that the issue had to do with a misunderstanding of how asp.net mvc works when passing data from server to client and back. Namely, I had an idea that my C# viewmodels and controllers on the server were still alive when their data was being sent to html/asp on the client. I had a hunch that the client data was not the same as the C# objects, but I did feel that ASP.Net MVC was updating the C# object for any changes on postback. Now it is clear to me that in fact the C# objects are fully discarded and completely instantiated (with the constructors called and all related consequences) and repopulated with data from client. And this is true even when no changes are made at the client.
I think that updates were in fact being made to the database. No rollback was occurring. But something was happening upon re-instantiation that was causing a second call to the database and resetting their values. This would explain why it was working perfectly outside of ASP.net MVC. It would explain why I solved the issue after this realization.
I would call this response accurate, but not precise. By that I mean that I have confidence that the guidance resolves the issue, even if it doesn't pin down the exact lines of offending code above. Due to the accuracy, I'm considering it fair game to mark it as an answer. Due to the imprecision, I'm open to marking someone else's response as the answer if they can be more precise. However, since the code above is no longer in use, it is all just for learning purposes.

Mocking Request.QueryString for Unit Tests and asserting against views

I have the following controller:
public class ResetController : Controller
{
//
// GET: /Reset/
private Models.ResetModel rm = new Models.ResetModel();
public ActionResult Index()
{
//Check that this has a query string that is containing in the database
//and has been done in the last 30 mins.
string qString = Request.QueryString["v"].ToString();
//if this is a good querystring
if (rm.CheckQString(qString))
return View();
else
return View("Index", "Home");
}
I now need to create a unit test to ensure that if the Request.QueryString value is found in the database then the appropriate view is returned but I am unable to do so. Here is my attempt at a test to check this:
[TestMethod()]
public void IndexTest()
{
ResetController target = new ResetController();
var request = new Mock<HttpRequestBase>();
request.SetupGet(r => r.QueryString).Returns(HttpUtility.ParseQueryString("?v=0ocIqhOQkrBaCXRO96E4B5HcOCYgMfJYOpRdNU/yIEUmH2szuXXKU51Td6NzRxlk"));
var result = target.Index() as ActionResult;
Assert.IsNotNull(result);
}
Can someone please help me with suggestions to ensure that this controller is fully tested?
Thanks
This is a late answer, but in the event that someone comes along this post in the future... Refer to this post how would I mock a querystring
The goal is to isolate the test so that it does not depend on the QueryString result from the database, but rather a provided value. To do this in Moq use the SetupGet method after creating a Mock Context. Hope this helps someone!
I would suggest you pass the model as a dependency to the controller. Then you can mock it as well in the unit test to isolate your controller logic from the model's CheckQString implementation logic.
I'm not sure though if I understand your problem correctly.
The good case might then look like this. Of course you would need to check if the correct view was returned.
[TestMethod()]
public void IndexTest()
{
const string query = "some query";
Models.ResetModel rm = new Mock<Models.ResetModel>();
rm.Setup(m => m.CheckQString(query)).Returns(true);
ResetController target = new ResetController(rm.Object);
var request = new Mock<HttpRequestBase>();
request.SetupGet(r => r.QueryString).Returns(HttpUtility.ParseQueryString("?v=" + query));
var result = target.Index() as ActionResult;
Assert.IsNotNull(result);
}

MVC3 delete action-name all

I have 3 action name delete on 3 different controllers which are on "registration,profile and questions" they all have action delete methods. How can I from my registration-delete method call out profile-delete and questions-delete . That way when a user wants to delete their account all they have to do is go on registration-delete instead of going on registration,profile and questions delete methods. I want in 1 [HttpPost, ActionName("Delete")](registration) to call out the other 2 ActionName("Delete") methods as i prefer people to delete everything in one place is this possible? assuming that each user shares a unique ID that is the same all across. The comment in the code im just using to illustrate any help would be great
registration-delete below
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed()
{
var ss = User.Identity.Name;
var getid = (from s in db.registration where ss == s.email select s.RegistrationID).FirstOrDefault();
registration registration = db.registration.Find(getid);
//This delete's the registration
db.buyers.Remove(registration);
// How can i call-out profile-delete actionname here and questions-delete like
//if (question-delete != null){
// include ActionResult deleteconfirmed("question-delete" }
db.SaveChanges();
return RedirectToAction("logout");
}
If I understand your question, you are asking to call other controller's action method from the currently executing action method? You typically would not do that. The first rule of Controller's action methods is: no action method should be more than 10 lines of code. Basically, an action method is really supposed to be a simple method to collect a view, or call an action in your domain, and return.
In other words, the SRP pattern:
http://codebetter.com/karlseguin/2008/12/05/get-solid-single-responsibility-principle/
Instead, you would organize your Domain-logic (what you describe is considered domain model logic, not controller logic) for this repetitive code such as deleting questions here, but when user is deleted also delete questions, etc.
// an example of IOC injection of your IUserService
private IUserService
public RegistrationController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public ActionResult Delete()
{
// insert modelstate and/or validation logic
//
if (User.Identity.IsAuthenticated == false)
{
return RedirectToAction("index", "Home");
}
// good practice to never bubble up exceptions to users
try
{
if (_userService.DeleteByEmail(User.Identity.Name) == false)
{
ModalState.AddModelError(String.Empty, "Not allowed?");
return View();
}
// save it all in a single atomic operation here to keep it all in
// a single Transaction Scope that will rollback properly if there is
// an error.
//
db.SaveChanges();
}
catch (Exception)
{
ModalState.AddModelError(String.Empty, "An error occurred...");
return View();
}
// all ok!
return RedirectToAction("logout");
}
Notice how clean this action method is. It just gets down to business with a single line of code or two, and a whole bunch of exit paths to handle the user's experience properly in many different situations.
Now, your domain logic can be encapsulated into a service (or provider, or alike):
namespace MyWebsite.Models
{
public class UserService : IUserService
{
// Todo: convert to some IoC lovin'
//
public Boolean DeleteByEmail(String email)
{
var user =
(from user in db.Registrations
where
user.Email == email
select s).FirstOrDefault();
if (user == null)
return false;
// call into other services to delete them
ProfileDataService.DeleteByUserId(user.UserId);
QuestionsService.DeleteByUserId(user.UserId);
// finally, delete the user
db.Registrations.Delete(user);
// note, I am NOT calling db.SaveChanges() here. Look
// back at the controller method for the atomic operation.
//
return true;
}
}
}
This can be implemented 100s of different ways. The point being is to abstract that logic out to a common code base, or "Domain". I chose to put that logic in your current Website namespace under Models as a shortcut in this example.
As for your other Delete() methods on your other controllers, you would just call into a QuestionsService.DeleteByUserId() and ProfileDataService.DeleteByUserId() to handle those. You can even share those services across the domain, as I showed above.

How do I test the default view when using T4MVC with MvcContrib.TestHelper AssertViewRendered

I am using T4MVC in my ASP.NET MVC 3 project. I have the following basic test:
[TestMethod]
public void IndexReturnsIndexView()
{
var controller = new HomeController();
var result = controller.Index();
result.AssertViewRendered().ForView(MVC.Home.Views.Index);
}
The test fails if the controller method returns the default View:
public virtual ActionResult Index()
{
return View();
}
The error given is:
MvcContrib.TestHelper.ActionResultAssertionException: Expected view name '~/Views/Home/Index.cshtml', actual was ''
But the test passes if I override the View to specify which viewName to return:
public virtual ActionResult Index()
{
return View(MVC.Home.Views.Index);
}
I tried using the following assertion, but still not luck:
result.AssertViewRendered().ForView(MVC.Home.Index().GetT4MVCResult().Action);
The following error is raised:
MvcContrib.TestHelper.ActionResultAssertionException: Expected view name 'Index', actual was ''
I then realized that I had misread the assertion failure, so I changed the test to this:
result.AssertViewRendered().ForView(String.Empty);
The test passes, but the test itself seems to be useless.
Preferably I don't want to have to specify all views by name, so how do I test this? To clarify, I am using the MvcContrib.Mvc3.TestHelper-ci 3.0.96.0, which I installed today from NuGet.
UPDATE
This isn't an answer to the question, but I have started doing the following instead, which provides more value as a test case:
using (var controller = new FeatureController(mockGateway))
{
// Act
var result = controller.Index();
var model = result.ViewData.Model as MyModel;
// Assert
Assert.IsNotNull(model, "Model is null or wrong type");
result.AssertViewRendered().WithViewData<MyModel>();
// Alternative Assert for model data
Assert.IsTrue(model.Items.Count > 0);
}
I left this open for a considerable amount of time, so that anyone else could answer if they chose to. I'll now answer this myself.
The following checks to make sure that the view returned contains the expected model, and more appropriately data within that model. This is a much better unit test for the controller in question.
using (var controller = new FeatureController(mockGateway))
{
// Act
var result = controller.Index();
var model = result.ViewData.Model as MyModel;
// Assert
Assert.IsNotNull(model, "Model is null or wrong type");
result.AssertViewRendered().WithViewData<MyModel>();
// Alternative Assert for model data
Assert.IsTrue(model.Items.Count > 0);
}
AssertViewRendered().ForView() tests against the view name that you explicitly pass to View() is your controller action. If, as you're doing, you don't specify a view name in your action, then .ForView() will be testing against an empty string, as you've seen.
If you called View("Index"); in your action, you could call .ForView("Index") in your test.
I would think that this assertion would be most useful if your action could return different possible views.

Save after Edit code improvements

I'm working on a simple application and am looking to get my head around the CodeFirst EF approach. So far so good.
I have managed to get Create and delete sorted and have got edit working. The thing is I think the edit code could be improved; I'm just not sure how. So here it is:
public ActionResult Edit(int id, CreateResourceViewModel model)
{
if (ModelState.IsValid)
{
// save the changes
//UpdateModel(model.Resource);
//resourceAdminManager.SaveChanges();
Resource current = resourceAdminManager.Resources.Find(id);
current.ResourceTypeID = model.Resource.ResourceTypeID;
current.Name = model.Resource.Name;
current.Description = model.Resource.Description;
current.Email = model.Resource.Email;
current.TurnAroundTime = model.Resource.TurnAroundTime;
resourceAdminManager.SaveChanges();
return RedirectToAction("Index");
}
else
{
return View(model);
}
}
I know there is no exception handling around this, which I need to address but my main concern is the fact that I have manually updated the model. My concerns are:
1. This is in the controller
2. This is hard coded and so any changes to the model will require a re-work of code
Can someone suggest a better way of doing this please.
Many thanks
Nathan
So following on from the AutoMapper suggestions:
This is very helpful and I've started to play with this. I'm running into a little trouble with it.
The contoller now looks like:
if (ModelState.IsValid)
{
try
{
var current = resourceAdminManager.Resources.Find(id);
current = Mapper.Map<CreateResourceViewModel, Resource>(model);
resourceAdminManager.SaveChanges();
return RedirectToAction("Index");
}
catch (Exception exc)
{
ModelState.AddModelError("Error", exc); // or, use a generic error.
}
}
return View(model);
The error occurs in the view when I click save. I get null exception on the following:
<%: Html.DropDownListFor(model => model.Resource.ResourceTypeID, new SelectList(Model.ResourceTypes, "ResourceTypeId", "Title"), "-- Select Resource Type --")%>
Any ideas on what I may be missing here?
One word: AutoMapper.
Will turn those 5 left-to-right boilerplate lines into one.
One of the best MVC (well, C#) add-on's i've ever found.
If implemented correctly (i think your ViewModel may need to change a tad), your action will look like this:
public ActionResult Edit(int id, CreateResourceViewModel model)
{
if (ModelState.IsValid)
{
try
{
var current = resourceAdminManager.Resources.Find(id);
current = Mapper.Map<CreateResourceViewModel,Resource>(model);
resourceAdminManager.SaveChanges();
return RedirectToAction("Index");
}
catch (Exception exc)
{
ModelState.AddModelError("Error", exc); // or, use a generic error.
}
}
return View(model);
}
A warning: this will basically replace the entire entity. So if you only want certain fields changed, only include those in the ViewModel, so the other's will be unchanged. You can also specify "Ignore" in AutoMapper so those fields won't get touched.
And to address your concerns:
This is in the controller
Well actually, it should be. In fact, that's the controllers job - updating the model.
The fact you have boilerplate L-R code is bad, which is why AutoMapper helps.
This is hard coded and so any changes to the model will require a re-work of code
Yup, again - AutoMapper solves this. It works on convention (like MVC), so if you have a proeprty in the ViewModel with the same name as the target model (domain object), you won't have to explicitly map -> it will just work. Furthermore, the mapping is a single static class, so it can be easily maintained.
You are using ViewModel which is very good practise.
1] For Exception handling will suggest to inherit your Controller
from your custom "BaseControllor" instead of System.Web.Mvc.Controller
public class YourController : BaseController
In "BaseControllor" override OnException so that all excptions across your Controllor Actions will be catch in it.
public class BaseController: Controller
{
protected override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
}
}
2] You need to refactor your "Save" code into differnt class in same project or differnt project and differnt Class
3] Yes for any Changes in model you would need to make changes in viewmodel, and in your save logic

Resources