0

The Data Model

enter image description here

The Entities

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string UrlSlug { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string UrlSlug { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string UrlSlug { get; set; }
    public string TnImage { get; set; }
    public string Author { get; set; }
    public List<Tag> Tags { get; set; }
    public Category Category { get; set; }
    public string DatePublished { get; set; }
    public string DateCreated { get; set; }
    public string DateModified { get; set; }
    public string Description { get; set; }
    public string ArticleBody { get; set; }
}

I'm using Entity-Framework against Sql Server Compact embedded DB, and have written a Generic Repository that does CRUD operations against the individual entities like Post, Category, Tag etc.

All that works fine.

I'm writing an ASP.Net Web API to expose the CRUD via a REST-ful API.

The REST API End Points

╔═══════╦═══════════════════════╦════════════════════════════════════╗
║ VERB  ║ End Point             ║  Description                       ║ 
╠═══════╬═══════════════════════╬════════════════════════════════════╣
║ GET   ║ /api/tags             ║ Returns all Tags                   ║
║ GET   ║ /api/tags/tips        ║ Returns single Tag (name=tips)     ║
║ POST  ║ /api/tags             ║ Creates new Tag                    ║
║ GET   ║ /api/categories       ║ Returns all Categories             ║
║ GET   ║ /api/categories/news  ║ Returns single Category (name=news)║
║ POST  ║ /api/categories       ║ Creates new Category               ║
║ GET   ║ /api/posts            ║ Returns all Post                   ║
║ GET   ║ /api/posts/51         ║ Returns single Post (Id=51)        ║
║ GET   ║ /api/posts/2/Tags     ║ Returns Tags for Post w/ Id=2      ║
║ GET   ║ /api/posts/2/Category ║ Returns Category for Post w/ Id=2  ║
║ POST  ║ /api/posts            ║ Creates new Post                   ║
╚═══════╩═══════════════════════╩════════════════════════════════════╝

The Situation

When I create a Post entity by doing a POST Request against the /api/posts endpoint and I have the Post entities data in the body as JSON, it could potentially have 1 or more tag instances, and 0 or 1 category.

Sample POST Body for a Blog Post

{
  "title": "How to get 6 Pack Abs in 6 days",
  "urlSlug": "6-pack-abs-6-days",
  "tnImage": "http:\/\/example.com\/image.jpg",
  "author": "John Doe",
  "tags": [
    {
      "name": "6 pack abs tips"
    },
    {
      "name": "exercise tips"
    },
    {
      "name": "workout videos"
    }
  ],
  "category": {
    "name": "fitness"
  },
  "datePublished": "2017-04-01",
  "dateCreated": "2015-01-20",
  "dateModified": "2017-04-01",
  "description": "SEO keyword stuffed description for fake tips to get abs here",
  "articleBody": "full post body containing fake tips to get 6 packs. html"
}

Of these tags, some may exist, and some may not. The ones that don't exist need to be created. And for the tags that need to be created, the UrlSlug will be generated using a helper method.

The category, if filled, should be inserted if it doesn't exist.

The question

Given this scenario, how do I had my POST request against the /api/posts endpoint, and ensure that the related Tag and Category entities are added if they don't exist?

If the sample post shown above is posted to this endpoint, how do I handle adding tags that don't exist, category if it doesn't exist, and then adding the new post?

Also, am I approaching this wrong in terms of REST design? If yes, what is the recommended approach to ensure that data isn't inconsistent, i.e. some tags added then error occurs, so there are orphans now.

public class PostsController : ApiController
{

    [Route("api/posts")]
    [HttpPost]
    public IHttpActionResult Post([FromBody]PostDto post)
    {
        try
        {
            if (post == null)
            {
                return BadRequest();
            }

            // what goes in here, so that I add tags that don't exist, 
            // category if it doesn't exist
            // and then create the post. All ADD methods will call repo methods
            // 
            // Also is there some other way, i.e. am I approaching this wrong?
        }
        catch (Exception)
        {
            return InternalServerError();
        }
    }
Shiva
  • 20,575
  • 14
  • 82
  • 112
  • 1
    The category can be verified and inserted if the verification returns null. for the tags, an iteration combined with an `.AddRange` should do the trick. – Cristian Szpisjak Apr 05 '17 at 19:29
  • Why you doubt about approach with checking for existence and adding? You can use transaction if you don't want create tags or category if something goes wrong. I think this have nothing to do with REST – Fabio Apr 05 '17 at 19:32
  • Thanks. @Fabio I say REST because modifications to multiple Resources are involved -- `Post`, `Tag`, `Category`. So I was wondering if there was someother `REST` approved approach.... Maybe I am overthinking this lol. – Shiva Apr 05 '17 at 21:04
  • 1
    Because creation of post can lead to possible creation of category or tag - you can include created or retrived id's(path) of category and tag to response. – Fabio Apr 05 '17 at 21:07

2 Answers2

1

I've done similar things in the past and where I did them was in the database. When calling a generic proc like "SetTag" it would take the entityid and the tag and then do a check query first like:

DECLARE @TagID INT
SELECT @TagID = TagID FROM TagTable WHERE Tag = @TagPassedIn
IF @TagID IS NULL 
BEGIN
INSERT INTO TagTable (Tag) VALUES (@TagPassedIn)
SELECT @TagID = SCOPE_INDENTITY()
END
INSERT INTO AttTable (EntityID,TagID) VALUES (@EntityIDPassedIn,@TagID)
  • 1
    This approach will move your "business logic" to database layer. While this can be working solution, it can be difficult to test and maintain. – Fabio Apr 05 '17 at 19:44
  • I would say it depends on what you're trying to do. If your aim to accept everything and add when needed then the database will be just fine. If you want to validate prior to adding and limit them, then it make sense to put business logic outside the database to keep it gated. Otherwise this works just great and keeps things simple. – Thomas Bright Apr 06 '17 at 16:13
1

A sample solution can be here:

using (var context = new YourContext())
            {
                //Find or Create Category
                Category category = null;
                if (context.Categories.Any(cat => cat.Name == post.category.Name))
                {
                    category = context.Categories.FirstOrDefault(cat => cat.Name == post.category.Name);
                }
                else
                {
                    category = new Category
                    {
                        Name = post.category.Name
                    };
                    context.Categories.Add(category);
                }

                //Find or Create Tags
                var tags = new List<Tag>();
                foreach (var tag in post.tags)
                {
                    Tag targetedTag;
                    if (context.Tags.Any(t => t.Name == tag.Name))
                    {
                        targetedTag = context.Tags.FirstOrDefault(t => t.Name == tag.Name);
                    }
                    else
                    {
                        targetedTag = new Tag
                        {
                            Name = tag.Name,
                            // UrlSlug = use helper as you've said 
                        };
                        context.Tags.Add(targetedTag);
                    }
                    tags.Add(targetedTag);
                }

                var targetedPost = new Post
                {
                    Category = category,
                    Tags = tags,
                    ArticleBody = post.articleBody,
                    Author = post.author,
                    DateCreated = post.dateCreated,
                    DateModified = post.dateModified,
                    DatePublished = post.datePublished,
                    Description = post.description,
                    Title = post.title,
                    TnImage = post.tnImage,
                    UrlSlug = post.urlSlug
                };

                context.Posts.Add(targetedPost);

                context.SaveChanges();

            }
Majid Parvin
  • 4,499
  • 5
  • 29
  • 47
  • Thanks @Majid ! This will work. I found a Generic re-usable Helper in this answer, the idea is essentially same as what you have outlined. http://stackoverflow.com/a/31162909/325521 – Shiva Apr 05 '17 at 21:18