0

Given the user sends a valid token to an api endpoint via fiddler/postman, he could post a resource (pupil) for a related resource (schoolclass).

When the schoolclass id

  • does not exist yet in the database
  • does exist already in the database but this schoolclass Id belongs to another user.
  • does exist in the database and belongs to the passed userId

Then

What would you change in the Controller and Repository class to make it work for all 3 cases using a REST api + repository pattern.

Controller:

[HttpPost("~/api/pupils")]
public async Task<IActionResult> Post([FromBody]CreatePupilRequestDto dto)
{
    var userId = User.GetUserId();
    var pupil = dto.ToPupil();
    await repository.CreatePupil(pupil, dto.SchoolclassId, userId);
    return Ok(pupil.Id);
}

Repository:

public async Task CreatePupil(Pupil pupil, int schoolclassCodeId, string userId)
{
    var schoolclassCode = await context.Schoolclasses.SingleOrDefaultAsync(s => s.Id == schoolclassCodeId && s.UserId == userId);
    if (schoolclassCode != null)
    {
        schoolclassCode.Pupils.Add(pupil);
        await context.SaveChangesAsync();
    }
}

NOTE

At the moment the last of the 3 use cases is implemented!

Pascal
  • 12,265
  • 25
  • 103
  • 195

2 Answers2

1

From REST prospective you need to return 400 or 404 depending on your design. If your route need to be like /classes/{id}/users/{id}/pupil I thing you need to use 404 in case user or class is wrong.

In case of separate route (as I can see in your question) I think this should be 400 code as request URL is pointing to valid resource but payload is invalid.

In both cases I think the batter error handling strategy here is to write some set of custom exceptions (like EntityNotFondException, EntityInvalidException, BusinessLogicException) and throw them from repository in case something is wrong. Then you can create some global action filter or OWIN middleware to catch those exceptions and translate them to correct response status codes with appropriate messages

Example:

public class NotFoundException : Exception
{
    public NotFoundException(Type entityType)
        : base($"Entity {entityType.Name} was not found")
    {
    }
}

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{

    public ApiExceptionFilterAttribute()
    {
    }

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        var exception = actionExecutedContext.Exception;
        if (exception == null)
            return;

        if (exception is HttpResponseException)
            return;

        var entityNotFoundException = exception as NotFoundException;
        if (entityNotFoundException != null)
        {
            actionExecutedContext.Response = actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, entityNotFoundException.Message);
            return;
        }
    }
}

Usage:

var schoolclassCode = await context.Schoolclasses.SingleOrDefaultAsync(s => s.Id == schoolclassCodeId && s.UserId == userId);
if(schoolclassCode == null)
   throw new NotFoundException(typeof(Schoolclass));

You can throw validation exceptions in the same way. E.g:

var schoolclassCode = await context.Schoolclasses.SingleOrDefaultAsync(s => s.Id == schoolclassCodeId);
if(schoolclassCode == null)
   throw new InvalidModelStateException("Schoolclass was not found.")

if(schoolclassCode.UserId != userId)
   throw new InvalidModelStateException("Schoolclass is owned by different user.")

... etc.

Alex Lyalka
  • 1,484
  • 8
  • 13
  • Funny I also thought about throwing exception from Repository and catch it in exceptionfilter, but I wasn`t sure about using exception in this case. I guess you mean "if(schoolclassCode.UserId != userId)" !equals ;-) , lets wait for other answers :-) – Pascal May 22 '17 at 21:39
  • if(schoolclassCode.UserId != userId)" !equals - Yes, sure. – Alex Lyalka May 22 '17 at 21:51
  • Exceptions are especially great in case you will have more layers here for example business logic layer between repositories and controllers. You can always wrap exceptions in more layer specific exceptions preserving original and transform them as you need. Usually people think that exception is bad for performance but this is not the case - we have really exception situation (data received from client is wrong) which is rare and really exceptional, - why not to use mechanism that aimed to solve this problem. – Alex Lyalka May 22 '17 at 21:59
  • You shouldn't use exceptions for flow control. Exceptions are, **as the name implies**, exceptional und should be thrown in situations when something unexpected happens. But not finding a user is something you expect – Tseng May 23 '17 at 05:58
  • Wait wait guys. Its not about finding a user like Get /user/1. Its about http post where the userId is always correct, but not the resource id like schoolclass id. See above... The question for a HttpPost is therefore is it exceptional when someone uses fiddler tool to send bad form data. I leave the answer open :-) – Pascal May 23 '17 at 18:11
  • My opinion is YES - this is definitely exception situation as we already did all possible validations: at first at client and, I think, some on the server using data annotation or something like FluentValidation and always expect that data that rich to repository is correct. Wrong class id its always result of mistake at some point (wrong api usage or something like that), like we got exception trying to open file that was deleted or inaccessible. – Alex Lyalka May 23 '17 at 18:45
  • I've used this aproach in list of commercial products and I've never had issues with this. @Tseng point of wraping data in some result object is make sense for me but I think it will increase complicity and add more If statements on each layer that data need to pass thru on the way to the client. Its doesnt meen you need to use exceptions to modify control flow - if they arise request will be finished with error no handling should prevent it. You can look at filter like on logger. So lets community judge. )) – Alex Lyalka May 23 '17 at 18:45
  • 1
    @AlexLyalka: In your example `schoolclassCodeId` (the school class entity) is the leading entity. If it's not found, 404/NotFound. If Invalid user (user doesn't exist or is already assigned to some other class) was passed, BadRequest since it's. That being said, your routes are not very REST-ful, they are more like RPC. A more restful route would be `~/api/schoolclasses/{schoolclassCodeId}/pupils`. Rest is about resources. `~/api/pupils` means, it's a list resource with many pupils and the when being restful it'd expect a model of type `Pupil`, but you pass in a Request (=command) – Tseng May 23 '17 at 18:59
  • 1
    @AlexLyalka: the suggested `~/api/schoolclasses/{schoolclassCodeId}/pupils` reads as: "for school class with the id `schoolclassCodeId` do something on it's pupil collection", which is what you want to do: (Post = add, get = read/filter (filter via query not route unless you select via unique id), put = update whole collection, `POST /api/schoolclasses/{schoolclassCodeId}/pupils/{pupilId}` = update specific pupil from that class). So with URL + Verbs you can exactly map it to a specific CRUD operation – Tseng May 23 '17 at 19:03
  • I agree with that: POST /api/schoolclasses/{schoolclassCodeId}/pupils/{pupilId} , I use that approach already for another url, but not yet on the pupils controller ;-) – Pascal May 23 '17 at 20:17
  • So we have 2 strong opinions here: Pass error code/message through the layers or throw exception and catch global in exception filter. Both make sense and both have the option to do a correct rest response. Puuhhh... – Pascal May 23 '17 at 20:22
  • On the other side I must say, I do mustly have repositories, just 2 services with logic. I can not integrate the QueryResult class in this repositories return value and I do not want to invent service classes for these repos... where in 99% of the cases the QueryResult is positive. I do not extend my architecture massively for 0.001% of the Use Case where a bad user sends fiddler posts. This is against KISS :-) Don`t you think so guys? – Pascal May 23 '17 at 20:41
  • I think we are going far and far away from initial topic. – Alex Lyalka May 23 '17 at 20:56
  • Anyway KISS is great principle and we all, as I think need to be simple, but mixing strategies in one project (I am talking about "I do mostly have repositories, just 2 services with logic") is looks not really good. You should think about this once again, thought . – Alex Lyalka May 23 '17 at 21:05
  • @Tseng: I am totally agree with your comments about REST – Alex Lyalka May 23 '17 at 21:06
  • @AlexLyalka Why should I create service classes which just forward data to/from the repository classes when not any logic is inside the service classes? Because of consistency? – Pascal May 24 '17 at 11:31
  • Yes. If you don't want to add some logic you can just use some generic. like: interface IService, class ServiceBase : IService { /* CRUD here */ }. You will use base implementation when logic is not required otherwise override some methods or add new. Its simple but more consistent to use one approach and its flexible if you need add login in future – Alex Lyalka May 24 '17 at 13:09
  • "if you neeed add login in future" don`t understand you. What has that to do with login? I already have oauth2 running here and see no problem with my approach. – Pascal May 24 '17 at 13:11
  • I mean it can work if you will have some generic interface for repositories as well – Alex Lyalka May 24 '17 at 13:11
  • *login - logic, sorry – Alex Lyalka May 24 '17 at 13:11
  • Making generic repos is a no go in genereal and in my case of no use as I have much logic in my queries/insert/delete/update methods which I can not outsource to any service class. Its a small data driven application. – Pascal May 24 '17 at 13:12
  • You have time for a chat now? http://chat.stackoverflow.com/rooms/145031/repository-security :-) – Pascal May 24 '17 at 13:13
1

I always use Result classes for returning state from a service class (wouldn't implement that in Repository as it shouldn't contain business logic):

public class QueryResult
{
    private static readonly QueryResult success = new QueryResult { Succeeded = true };
    private readonly List<QueryError> errors = new List<QueryError>();

    public static QueryResult Success { get { return success; } }

    public bool Succeeded { get; protected set; }
    public IEnumerable<QueryError> Errors { get { return errors; } }

    public static QueryResult Failed(params QueryError[] errors)
    {
        var result = new QueryResult { Succeeded = false };
        if (errors != null)
        {
            result.errors.AddRange(errors);
        }

        return result;
    }
}

public class QueryResult<T> : QueryResult where T : class
{
    public T Result { get; protected set; }

    public static QueryResult<T> Suceeded(T result)
    {
        if (result == null)
            throw new ArgumentNullException(nameof(result));

        var queryResult = new QueryResult<T>
        {
            Succeeded = true,
            Result = result
        };

        return queryResult;
    }
}

public class QueryError
{
    public string ErrorId { get; set; }
    public string ErrorMessage { get; set; }
}

And use it like

var schoolclassCode = await context.Schoolclasses
    .SingleOrDefaultAsync(s => s.Id == schoolclassCodeId && s.UserId == userId);
if (schoolclassCode == null)
    return QueryResult.Failed(new QueryError
    {
        ErrorId = 1,
        ErrorMessage = "Invalid User Id"
    });

Edit:

Just as an addition and rule of thumb

  1. Services which operate on one or multiple entities and perform user input validation should return Result classes
  2. Domain Models (which you don't seem to use, since you use a repository and Repository + Rich Domains doesn't work out well in real life applications) should throw exception (i.e. InvalidOperationException or ArgumentException, ArgumentNullException). Doing Result-types her will pollute the model and mix the separation of responsibility (Domain Model will suddenly also do validation instead only guarding against invalid state)

Using XxxResult type classes gives you an easy way to transport one or multiple errors back to the user, where an exception should act as an guard against your domain model getting into invalid state.

Edit 2

In response to the comments:

public async Task<IActionResult> Post([FromBody]CreatePupilRequestDto dto)
{
    var userId = User.GetUserId();
    var pupil = dto.ToPupil();
    var result = await repository.CreatePupil(pupil, dto.SchoolclassId, userId);

    // If you want to suppress the error messages, just call return BadRequest() instead
    if(!result.Succeeded)
        return BadRequest(result.Errors);

    return Ok(pupil.Id);
}

Edit 3

Example with 3 parameters for let's say /api/schoolclasses/1/students/2/lessons/2 (Update an existing lesson to the student with the id 2 for the school class with id 1).

// on SchoolClasses Controller
[HttpPost("{schoolClassId:int}/students/{studentId:int}/lessons/{lessonId:int}")]
public async Task<IActionResult> Post([FromBody]Lessons lessonDto)
{
    // rough input validation, do first to avoid db hits
    if(!ModelState.IsValid)
        return BadRequest(ModelState);

    // best put logic into service classes i.e. SchoolClassService
    var result = schoolClassService.UpdateLessonFor(schoolClassId, studentId, lessonDto)

    // If you want to suppress the error messages, just call return BadRequest() instead
    if(!result.Succeeded)
        return BadRequest(result.Errors);

    return Ok();
}

Content of UpdateLessonsFor

List<ErrorMessage> errors = new List<ErrorMessage>();

// with .Include to include both student and all of his lessons
// does student exist? 
// Hits db once and gets both, student and all lessons in a single query
var student = _context.SchoolClasses
                  .Include(sc => sc.Students)
                       .ThenInclude(s => s.Lessons)
                  .Where(sc => sc.SchoolClassId == schoolClassId)
                  .SelectMany(sc => sc.Students)
                  FirstOrDefault(s => s.StudentId == studentId);

if(student==null)
    return QueryResult.Failed( new ErrorMessage { ErrorId = 1, ErrorMessage = "Student or School Class not found" } );

// Doesn't hit the database, since lessons have been loaded with the above call
var lesson = student.Lessons.Any(l => l.LessonId = lessonId))
if(lesson == null)
    return QueryResult.Failed( new ErrorMessage { ErrorId = 2, ErrorMessage = "Lesson not found. " } );

// modify it
lesson.SomeValue = dto.SomeValue;

try
{

} catch(Exception ex) {
    return QueryResult.Failed(new ErrorMessage { ErrorId = 3, ErrorMessage = "Couldn't update the lesson. Try again and if the error appears again, contact the administrator." } );
} finally {
    return QueryResult.Suceeded;

    // or if you also want to return a result
    return QueryResult.Suceeded(lesson);
}

Also from the comments of the other answer: Don't put logic into your repository, that's what services are for when you use anemic domain (models have no logic, all in services) or have thin service layer and put most logic into domain service. But that's out of the scope.

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • How would your controller action looke like considering a bad and good result from a domain service doing that http post? Actually I do not want to let the bad user know that he update was not made because the resource id was wrong. I want to let him be in the dark ;-) – Pascal May 23 '17 at 18:17
  • Yes I don`t domain models (with behavior) just persistance entities. My app is data driven not really behavior driven. I have already gotten an advice here on SO - another question - about a Domain Result class returning from a service. Now I must ask the question: Do you have a link about this architecture/pattern? – Pascal May 23 '17 at 18:29
  • That makes no sense. When the user (or rather the client) passes wrong data, he should know why. Everything else is bad design. Your rest service should perform the necessary steps to prevent users from forging requests or bypassing some logics. Not showing an error message won't give you any security advantage at all, but will only confuse the consumers/users of your API: "I added it and there was no error, but wasn't added" and then "you/the company" needs to look into it and waste time (and hence money) for useless support queries – Tseng May 23 '17 at 18:35
  • Have no links, but it's common way to transport validation errors back to the UI. Exceptions imply an performance penalty, when it happens frequently. Hence the exceptions should be **exceptional** and it's bad design. – Tseng May 23 '17 at 18:36
  • But don`t you think in my case it is exceptional see: https://stackoverflow.com/questions/44121971/what-should-my-repository-return-on-a-http-post-when-the-posted-id-foreign-key/44126990?noredirect=1#comment75302338_44122343 – Pascal May 23 '17 at 18:37
  • No, wrong input results from a web or rest services are always expected, because anyone using your API could accidentally (you only explain the intentionally part) implement it wrongly or pass a wrong value. Golden rule when you create a server sided application: **NEVER trust the client**. This is to be taken literally, even if the client is written by you, the api is still open – Tseng May 23 '17 at 18:43
  • Well, if all 3 ids are **required**,yes.if not,only for the required ones,though this may be able to be done with one query (via joins/includes), depending on how fine grained you want the error messages to be (i.e. `Invalid User Id`, `Invalid Class Id`, etc.). There are not many http codes you can return anyways when he client does something wrong (only 4xx codes, 5xx are for server-sided errors). From the 4xx errors there are only 400 (bad request = invalid input), 401, 403, 404 (not found), 409 conflict (i.e. record already exists in DB or has been changed (different concurency token)) – Tseng May 26 '17 at 11:57
  • Sorry for deleting my comment a second before you answered. I created a new question which is much more narrow or more concrete to answer I think. Bitte schön: :-) https://stackoverflow.com/questions/44202286/should-server-side-dto-validation-validate-the-existance-of-data-or-the-integrit – Pascal May 26 '17 at 13:01
  • Sorry Tseng for late answer. Just have seen your 'Edit 3'. You said you would not implement the DomainResult in the Repository but in the domain service like schoolclassService, but then your context is inject into the schoolclass service right? not into a repository. But then you have DataAccess code in your domain service. Where is your UpdateLessonsFor method implemented? SchoolclassService or SchoolclassRepository ? – Pascal Jun 05 '17 at 15:23
  • `DomainResult` should be outside of repository, though the actual queries can stay/be abstracted in an repository. The reason is in order to generate error messages for `DomainResult` in case of failure you need certain validation/business logic and this doesn't belong to a repository – Tseng Jun 05 '17 at 15:29
  • Ok we have a better common understanding. But then one more question arise: When the Repository KNOWS the multiple error situations like student or lesson is null, BUT does NOT KNOW the Query/DomainResult class, how do you communicate these multiple errors down from the Repository to the Domain Service? Another RepositoryResult class? :P – Pascal Jun 05 '17 at 15:36
  • You **don't** return `DomainResult` (or `QueryResult` or whatever you want to call it) form the repository, but from a domain service. The domain service then access the repository to retrieve the data. Repository only returns entities (or `null` when not found), i.e. `var student = studentsRepository.Get(studentId)`. The domain service validates it in the context of the method. Repository is only for data retrieval and abstraction of the persistence layer and should contain **no logic**. When updating fails you catch the exception in the domain service and handle it – Tseng Jun 05 '17 at 15:42
  • You have not understood my question. First I agree to all what you lastly said. Now I rephrase my question: When my repository.UpdateLessonsFor(x,y,z) return a null value, from where do I know in the schoolclassService that the returning null is from student OR lesson because I need to know that in the service to map it to the correct DomainResult. Hope its more clear now :-) – Pascal Jun 05 '17 at 15:47
  • An update method shouldn't return an entity, it should be `void` return type. When the update fails you get an exception when it fails (constrain validation, non-existing record, `DbConcurrencyException` occurs, etc.). You could implement an `DbResult` or whatever, but that makes it harder to handle concurrency, since the `Result` classes just tell you if an error happend and shows an error message which is useful for the user, so `Result` classes are just a means of transporting this errors to a point where you can show then `DbConcurrencyException` however can be used to retry the operation – Tseng Jun 05 '17 at 16:16