JsonApiFramework icon indicating copy to clipboard operation
JsonApiFramework copied to clipboard

[Help needed] Provide a POST example

Open RichardsonWTR opened this issue 3 years ago • 17 comments

Imagine this situation: the client of the project Blogging Tutorial from the samples repo sends this message to the server:

{
  "data": {
    "type": "articles",
    "attributes": {
      "title": "JSON API paints my house!",
      "text": "If you’ve ever argued with your team about the way your JSON responses should be
               formatted, JSON API can be your anti-bikeshedding tool."
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "9" }
      },
      "blogs": {
        "data": { "type": "blog", "id": "1" }
      }
    }
  }
}

In the samples repo the post/put/delete functions aren't implemented:

[HttpPost("articles")]
public Document Post([FromBody]Document inDocument)
{
	throw new NotImplementedException();
}


[HttpPatch("articles/{id}")]
public Document Patch(string id, [FromBody]Document inDocument)
{
	throw new NotImplementedException();
}


[HttpDelete("articles/{id}")]
public void Delete(string id)
{
	throw new NotImplementedException();
}

I was trying to insert a new element by transforming a Document object to a Article object with the relationships.
This is a piece of code of the implementation I was trying.

HttpPost("articles")]
public ActionResult<Document> Post([FromBody]Document inDocument)
{
	using (var documentContext = new BloggingDocumentContext(Request.GetUri(), inDocument))
	{
		Article a = documentContext.GetResource(typeof(Article)) as Article;

		var relationsships = documentContext.GetResourceRelationships<Article>();

		ToOneRelationship t = relationsships["author"] as ToOneRelationship;
		a.AuthorId = t.Data.Type.Equals("person") ? long.Parse(t.Data.Id) : 0;

		t = relationsships["blogs"] as ToOneRelationship;
		a.BlogId = t.Data.Type.Equals("blogs") ? long.Parse(t.Data.Id) : 0;

		ModelState.Clear();
		if (TryValidateModel(a) == false)
		{
			// TODO list errors contained in ModelState and return it in the BadRequest.
			return BadRequest();
		}
		BloggingRepository.AddArticle(a);
	}

	return NoContent();
}

What do you think? Am I in the right path? Any help would be appreciated. Regards

RichardsonWTR avatar Aug 04 '20 20:08 RichardsonWTR

@RichardsonWTR I'll get back to you soon, was away...

scott-mcdonald avatar Aug 06 '20 04:08 scott-mcdonald

It is a funny thing, based on another issue you raised I have recently started creating ASP.NET Core 3.1 hypermedia API's, etc. So with that said and your request here, I think it is time I first upgrade this sample to ASP.NET Core 3.1 and add POST, PATCH, and DELETE examples as well. As you discovered in another thread, you need to manually add JSON.NET as the JSON serializer/deserializer as the framework was developed and thus integrated with JSON.NET.

As a sidebar, a better approach would be to abstract away the JSON serialization/deserialization so the framework is not directly coupled with any specific JSON serializer, etc. I have done that on other projects but unfortunately this framework is directly integrated with JSON.NET. Not the end of the world though as JSON.NET is pretty good and has served this framework well for a long time...

Ok, will start enhancing the blogging tutorial, etc.

scott-mcdonald avatar Aug 09 '20 18:08 scott-mcdonald

Thank you for your hard work with this library.
I apreciate your attention.
Looking forward for updates.

RichardsonWTR avatar Aug 11 '20 17:08 RichardsonWTR

@RichardsonWTR

JsonApiFramework does a lot of the heavy lifting when it comes to dealing with JSON:API, what you are trying to do can be done somewhat like this.

        [HttpPost("articles")]
        public IActionResult Post([FromBody] Document inDocument)
        {
            var displayUrl = _httpContextAccessor.HttpContext.Request.GetDisplayUrl();
            var currentRequestUri = new Uri(displayUrl);

            using var bloggingDocumentContext = new BloggingDocumentContext(currentRequestUri, inDocument);
            var articleResource = bloggingDocumentContext.GetResource<Article>();
            var article = new Article
            {
                ArticleId = RandomData.GetInt(100,1000),
                Text = articleResource.Text,
                Title = articleResource.Title
            };

            /////////////////////////////////////////////////////
            // Get all relationships for article resouce, then verify the relationship has linkage.
            /////////////////////////////////////////////////////
            var resourceRelationships = bloggingDocumentContext.GetResourceRelationships<Article>();
            var hasRelationshipToAuthor = resourceRelationships.TryGetRelationship("author", out var authorRealtionship);
            if (hasRelationshipToAuthor && !authorRealtionship.IsResourceLinkageNullOrEmpty())
            {
                var authorResourceLinkage = authorRealtionship.GetToOneResourceLinkage();
                article.AuthorId = long.Parse(authorResourceLinkage.Id);
            }

            var hasRelationshipToBlog = resourceRelationships.TryGetRelationship("blogs", out var blogsRealtionship);
            if (hasRelationshipToBlog && !blogsRealtionship.IsResourceLinkageNullOrEmpty())
            {
                var blogResourceLinkage = blogsRealtionship.GetToOneResourceLinkage();
                article.BlogId = long.Parse(blogResourceLinkage.Id);
            }

            BloggingRepository.AddArticle(article);

            /////////////////////////////////////////////////////
            // Build JSON API document
            /////////////////////////////////////////////////////
            var document = bloggingDocumentContext
                .NewDocument(currentRequestUri)
                    .SetJsonApiVersion(JsonApiVersion.Version10)
                    .Links()
                        .AddUpLink()
                        .AddSelfLink()
                    .LinksEnd()
                    .Resource(article)
                        .Relationships()
                            .AddRelationship("blog", new[] { Keywords.Related })
                            .AddRelationship("author", new[] { Keywords.Related })
                            .AddRelationship("comments", new[] { Keywords.Related })
                        .RelationshipsEnd()
                        .Links()
                            .AddSelfLink()
                        .LinksEnd()
                    .ResourceEnd()
                .WriteDocument();
            return Created(document.GetResource().SelfLink(), document);
        }

You can run the .NET Core 3.1 version of this sample by running the unit test project found here

@scott-mcdonald Not sure how far you are into updating the samples project, but if I get a chance I can add the post/delete to my fork since I already updated the project, then I can open a PR against your branch.

I'll leave you to handle PATCH :)

circleupx avatar Aug 12 '20 02:08 circleupx

@RichardsonWTR @circleupx So I have an intermediate commit that has the POST and DELETE example implementations. I upgraded the ASP.NET Core project to 3.1 and introduced some higher-level things such as using Sqlite in-memory database and server-side validation of incoming requests, etc. This is not quite production quality code but a more substantial example of the CUD aspect of the framework, etc. The one thing I have not tackled yet is how PATCH is going to work so I have left that as a not implemented for now.

scott-mcdonald avatar Aug 18 '20 20:08 scott-mcdonald

@scott-mcdonald

Any reason why used an exception filter instead of middleware to do error handling?

circleupx avatar Aug 18 '20 20:08 circleupx

And thank you for sharing. Especially the ErrorHandling code.

circleupx avatar Aug 18 '20 20:08 circleupx

@circleupx Personally I was unaware of any middleware for error handling. Hooking into the ASP.NET messages and filter extension points are what I am most familiar with so just following my previous implementation techniques. I will say I have introduced some "middleware" to normalize query parameters so when the server generates hypermedia the order of query parameters is consistent. I did this because I want to use the URL as cache keys so the normalization allows for greater cache key hits, etc. Is the middleware the recommended best practice for error handling? In general, when should we integrate with messages and/or filters versus middleware extension points? I don't have the answers to those questions so I might need to do some research on said topics... Thanks for the feedback ;-)

scott-mcdonald avatar Aug 18 '20 21:08 scott-mcdonald

@scott-mcdonald

I believe a middleware is now the recommended best practice when doing error handling because .NET Core filters are very specific to MVC, where a middleware will catch an error at any point of the .NET Core stack. For example, assume your API exposes a GraphQL endpoint, kinda like this,

class Program
{
    static async Task Main(string[] args)
    {
        var app = WebApplication.Create(args);

        app.Map("/graphql", async http =>
        {
            // Implementation details
        });

        await app.RunAsync();
    }
}

and a client sends an HTTP request to that /GraphQL endpoint, well if the API generates an error, then it will bubble up to the error handling middleware for you to handle the error, just like how you did it for the exception filter, this is because of the fact that all HTTP request/response pass through all .NET Core middlewares.

If you were doing an Exception filter, the exception would never bubble up to the filter because filters are very specific to MVC, and by MVC I mean controllers.

At least that has always been my understanding of middlewares vs filters, someone can correct me if I am wrong.

So when it comes to global error handling I favor using a middleware, possibly like this,

     // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
    public class ExceptionHandlingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext httpContext)
        {

            try
            {
                await _next(httpContext);
            }
            catch (Exception exception)
            {
                await HandleException(httpContext, exception);
            }
        }

        private Task HandleException(HttpContext httpContext, Exception exception)
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var errorsDocument = new ErrorsDocument();
            var eventTitle = exception.GetType().Name;
            var randomEventId = GetEventId();
            var eventId = new EventId(randomEventId, eventTitle);
            var errorException = new ErrorException(Error.CreateId(eventId.ToString()), HttpStatusCode.InternalServerError, Error.CreateNewId(), eventTitle, exception.Message, exception.InnerException);

            errorsDocument.AddError(errorException);
            return httpContext.Response.WriteAsync(errorsDocument.ToJson());
        }
    }

circleupx avatar Aug 18 '20 23:08 circleupx

@circleupx Thank you for the information. What you are saying makes sense as they are now generalizing (in 3.1) ASP.NET Core and MVC is an "extension". I'll look into this further but I'll deprecate the exception filter in favor of exception middleware for the sample and my production projects as well. Thanks again!

scott-mcdonald avatar Aug 19 '20 15:08 scott-mcdonald

Thank you all for your support!
I'm not active in this post lately but I'm aware of all the updates.
I have a draft for Patch. I will update the samples project by the end of this week

RichardsonWTR avatar Aug 20 '20 11:08 RichardsonWTR

@RichardsonWTR

Nice, can't wait to see how you handled PATCH.

My use case was simple, so I used another library, SimplePatch. See if that helps you.

circleupx avatar Aug 20 '20 12:08 circleupx

A promise is a promise.

Sorry.. late by more than one week later. I created a PR with my solution to handle the PATCH request.
Please read the details in the mentioned PR.

This library is very powerful but we still have to do a lot of manual work. In the following days I'll share my thoughts about it.

RichardsonWTR avatar Aug 28 '20 03:08 RichardsonWTR

I just took a look at the SimplePatch library. Basically it's what I was trying to do.
I guess I could just replace the entire body method with a simple SimplePatch call and voilà! PATCH updates are supported natively! (kinda)

What do you think about it @scott-mcdonald ?

RichardsonWTR avatar Aug 28 '20 03:08 RichardsonWTR

@RichardsonWTR @circleupx I have not had a chance to look at PATCH yet due to current job pressures, BUT coincidentally we will need PATCH soon for our API so I will be taking a close look at this soon as well. Thanks for the pull request...

scott-mcdonald avatar Aug 31 '20 15:08 scott-mcdonald

@scott-mcdonald

That is going to be an awesome feature for JsonApiFramework.

It would help me remove my dependency on SimplePatch. Speaking of SimplePatch, I like their configuration approach, it gives developers the power to control how their models are mapped. It would be awesome to have something similar in JsonApiFramework.

Let me know if I can help.

circleupx avatar Sep 02 '20 16:09 circleupx

@RichardsonWTR Sorry I have not had time to look at the PATCH implementation that is being suggested in the JsonApiFramework samples yet as I have been so damn busy my head is spinning. Currently where in my job where I have needed PATCH in a few places I have done a manual implementation which is not what I want in the end. I will get to this eventually but probably not until we have a releasable product at my work... So sorry...

scott-mcdonald avatar Dec 23 '20 16:12 scott-mcdonald