Search code examples
c#.netasp.net-mvcarchitecturen-tier-architecture

How should I return a generic response and response code from all functions in a .NET MVC app?


I want to be able to return a generic response from function calls in the business layer of my MVC application. Most of the time I see an object create function look like this

 public int Create(ICNUser item)
 {
       return this._repository.Create(item);
 }
 public void Update(ICNUser item)
 {
      this._repository.Create(item);
 }

In this case the _repository is a repository that wraps entity framework.

This works great for a lot of cases but I want more information to be returned and I want to have a success/failure variable and a response code for why this action failed validation. I want to optionally be able to return the inserted object or a selected object.

An example would be a create user function that returns an email can't be blank error and or a user already exists error and based on the error I show the user a different message.

The problem I'm running into is I want to have unit tests cover all of the possible response codes from a function without me having to go look at the code and try to figure out what the possible return values can be. What I'm doing feels like an anti-pattern. Is there a better way to accomplish all of this?

This is what I have now.

 public IGenericActionResponse<ICNUser> Create(ICNUser item)
 {
        return this._repository.Create(item);
 }

 public IGenericActionResponse Update(ICNUser item)
 {
       return this._repository.Update(item);
 }

Interfaces

 namespace Web.ActionResponses
 {



public enum ActionResponseCode
{
    Success,
    RecordNotFound,
    InvalidCreateHash,
    ExpiredCreateHash,
    ExpiredModifyHash,
    UnableToCreateRecord,
    UnableToUpdateRecord,
    UnableToSoftDeleteRecord,
    UnableToHardDeleteRecord,
    UserAlreadyExists,
    EmailCannotBeBlank,
    PasswordCannotBeBlank,
    PasswordResetHashExpired,
    AccountNotActivated,
    InvalidEmail,
    InvalidPassword,
    InvalidPageAction
}

public interface IGenericActionResponse
{
    bool RequestSuccessful { get; }
    ActionResponseCode ResponseCode { get; }
}

public interface IGenericActionResponse<T>
{
    bool RequestSuccessful { get; }
    bool RecordIsNull{get;}
    ActionResponseCode ResponseCode { get; }
}
}

implementations

namespace Web.ActionResponses
{

public class GenericActionResponse<T> : IGenericActionResponse<T>
{
    private bool _requestSuccessful;
    private ActionResponseCode _actionResponseCode;
    public T Item { get; set; }
    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode, T item)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;
        this.Item = item;
    }

    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;
        this.Item = default(T);
    }

    public bool RecordIsNull
    {
        get
        {
            return this.Item == null;
        }
    }

    public bool RequestSuccessful
    {
        get
        {
            return this._requestSuccessful;
        }
    }

    public ActionResponseCode ResponseCode
    {
        get
        {
            return this._actionResponseCode;
        }
    }

}




public class GenericActionResponse : IGenericActionResponse
{
    private bool _requestSuccessful;
    private ActionResponseCode _actionResponseCode;

    public GenericActionResponse(bool success, ActionResponseCode actionResponseCode)
    {
        this._requestSuccessful = success;
        this._actionResponseCode = actionResponseCode;

    }

    public bool RequestSuccessful
    {
        get
        {
            return this._requestSuccessful;
        }
    }

    public ActionResponseCode ResponseCode
    {
        get
        {
            return this._actionResponseCode;
        }
    }

}}

MVC app

public ActionResult ValidateResetHash(string passwordResetHash)
    {
        IGenericActionResponse result = (IGenericActionResponse)this._userManager.IsValidPasswordResetHash(passwordResetHash);

        if (result.RequestSuccessful)
        {
            Models.PasswordChangeModel model = new Models.PasswordChangeModel();
            model.PasswordResetHash = passwordResetHash;
            return View("~/Areas/Public/Views/ResetPassword/PasswordChangeForm.cshtml", model);
        }
        else
        {
            switch (result.ResponseCode)
            {
                case ActionResponseCode.RecordNotFound:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/Login", "Login", "You have submitted an invalid password reset link.", false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
                case ActionResponseCode.PasswordResetHashExpired:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/ResetPassword", "Reset Password", "You have submitted an expired password reset link. You must reset your password again to change it.", false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
                default:
                    {
                        FermataFish.Models.GenericActionModel responseModel = new FermataFish.Models.GenericActionModel(true, "/", "Home", "An unknown error has occured. The system administrator has been notified. Error code:" + Enum.GetName(typeof(ActionResponseCode), result.ResponseCode), false);
                        return View("~/Views/Shared/GenericAction.cshtml", responseModel);
                    }
            }
        }
    }

Solution

  • The switch statement in your ValidateResetHash response is a tad code smelly. This would suggest to me that you may benefit from the use of a subclassable enum. The subclassable enum would map action response codes or types to return views with models. Here is a compiling example of how to use this.

    First some class fills I used to get a compiling example:

    public class GenericActionModel
    {
        private bool v1;
        private string v2;
        private string v3;
        private string v4;
        private bool v5;
    
        protected GenericActionModel() {}
        public GenericActionModel(bool v1, string v2, string v3, string v4, bool v5)
        {
            this.v1 = v1;
            this.v2 = v2;
            this.v3 = v3;
            this.v4 = v4;
            this.v5 = v5;
        }
    }
    
    public class ActionResult
    {
        private GenericActionModel responseModel;
        private string v;
    
        public ActionResult(string v, GenericActionModel responseModel)
        {
            this.v = v;
            this.responseModel = responseModel;
        }
    }
    
    public class PasswordChangeModel : GenericActionModel
    {
        public object PasswordResetHash
        {
            get;
            set;
        }
    }
    
    public interface IUserManager
    {
        Response IsValidPasswordResetHash(string passwordResetHash);
    }
    

    Next some infrastructure(framework) classes (I'm using StringEnum base class from the AtomicStack project for the ResponseEnum base class):

    public abstract class Response
    {
        public abstract string name { get; }
    }
    
    public class Response<TResponse> : Response where TResponse : Response<TResponse>
    {
        private static string _name = typeof(TResponse).Name;
        public override string name => _name;
    }
    
    // Base ResponseEnum class to be used by more specific enum sets
    public abstract class ResponseEnum<TResponseEnum> : StringEnum<TResponseEnum>
        where TResponseEnum : ResponseEnum<TResponseEnum>
    {
        protected ResponseEnum(string responseName) : base(responseName) {}
        public abstract ActionResult GenerateView(Response response);
    }
    

    Here are some sample responses:

    public class HashValidated : Response<HashValidated>
    {
        public string passwordResetHash;
    }
    public class InvalidHash : Response<InvalidHash> {}
    public class PasswordResetHashExpired : Response<PasswordResetHashExpired> {}
    public class Unexpected : Response<Unexpected> {}
    

    A sample subclassable enum mapping the sample responses would look something like this:

    public abstract class ValidateHashResponses : ResponseEnum<ValidateHashResponses>
    {
    
        public static readonly ValidateHashResponses HashOk                     = HashValidatedResponse.instance;
        public static readonly ValidateHashResponses InvalidHash                = InvalidHashResponse.instance;
        public static readonly ValidateHashResponses PasswordResetHashExpired   = PasswordResetHashExpiredResponse.instance;
        public static readonly ValidateHashResponses Default                    = DefaultResponse.instance;
    
        private ValidateHashResponses(string responseName) : base(responseName) {}
    
        protected abstract class ValidateHashResponse<TValidateHashResponse, TResponse> : ValidateHashResponses
            where TValidateHashResponse : ValidateHashResponse<TValidateHashResponse, TResponse>, new()
            where TResponse : Response<TResponse>
        {
            public static TValidateHashResponse instance = new TValidateHashResponse();
            private static string name = Response<TResponse>.Name;
            protected ValidateHashResponse() : base(name) {}
        }
    
        protected class HashValidatedResponse : ValidateHashResponse<HashValidatedResponse, HashValidated>
        {
            public override ActionResult GenerateView(Response response)
            {
                PasswordChangeModel model = new PasswordChangeModel();
                model.PasswordResetHash = ((HashValidated) response).passwordResetHash;
                return new ActionResult("~/Areas/Public/Views/ResetPassword/PasswordChangeForm.cshtml", model);
            }
        }
    
        protected class InvalidHashResponse : ValidateHashResponse<InvalidHashResponse, InvalidHash>
        {
            public override ActionResult GenerateView(Response response)
            {
                GenericActionModel responseModel = new GenericActionModel(true, "/Login", "Login", "You have submitted an invalid password reset link.", false);
                return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
            }
        }
    
        protected class PasswordResetHashExpiredResponse : ValidateHashResponse<PasswordResetHashExpiredResponse, PasswordResetHashExpired>
        {
            public override ActionResult GenerateView(Response response)
            {
                GenericActionModel responseModel = new GenericActionModel(true, "/ResetPassword", "Reset Password", "You have submitted an expired password reset link. You must reset your password again to change it.", false);
                return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
            }
        }
    
        protected class DefaultResponse : ValidateHashResponses
        {
            public static DefaultResponse instance = new DefaultResponse();
            private DefaultResponse() : base("Default") {}
            public override ActionResult GenerateView(Response response)
            {
                GenericActionModel responseModel = new GenericActionModel(true, "/", "Home", "An unknown error has occured. The system administrator has been notified. Error code:" + response.name, false);
                return new ActionResult("~/Views/Shared/GenericAction.cshtml", responseModel);
            }
        }
    
    }
    

    Implementing the SampleController:

    public class SampleController
    {
        private IUserManager _userManager;
        public ActionResult ValidateResetHash(string passwordResetHash)
        {
            Response    result      = this._userManager.IsValidPasswordResetHash(passwordResetHash);
            var         resultType  = ValidateHashResponses.TrySelect(result.name,ValidateHashResponses.Default);
            return resultType.GenerateView(result);
        }
    
    }
    

    Tweak the code above to fit your situation.

    If you want to allow others to extend the ValidateHashResponses enum, you can make the constructor protected instead of private. They can then extend ValidateHashResponses and add their own additional enum values.

    The point of using the subclassable enum, it to take adavantage of the TrySelect method that resolves responses to a specific enum value. Then we call the GenerateView method on the enum value to generate a view.

    Another benefit of the enum is that if you need to have other decisions made based on the enum value, you simply add another abstract method to the enum and all value definitions will be forced to implement the new abstract method, unlike traditional enum/switch statement combinations where new enum values are not required to have cases added and where one may forget to revisit all of the switch statements where the enum was used.

    DISCLAIMER: I'm am the author of the AtomicStack project. Feel free to take the Subclassable enum class code from the project if you feel it would suit your needs.

    UPDATE:

    If you want to inject the response enum, you should create an IResponseHandler adapter interface with a GenerateViewForResponse type method and provide a concrete implementation that consumes the ValidateHashResponses enum.