1

ASP.NET MVC 4 web app, EF 5, SQL Server 2012 Express, Visual Web Developer 2012 Express, Code First

I have a places object - I would like to add multiple tags to each place.

Is the best approach a comma separated string or related object?

Any design patterns or ways to manage the whole thing (adding, looking up, associating with a place etc)? Good performance is also of interest.

Tagging is done by administrators so speed / ease of implementation at the cost of a little ease of use is acceptable.

Thanks.

niico
  • 11,206
  • 23
  • 78
  • 161

2 Answers2

2

Firstly, I don't think you need an object to represent a tag - assuming the tag is just a string.

What you should do is add a generic list property of strings to your entity like so:

public IList<string> Tags { get; set; }

Entity Framework will handle it just fine.

About the UI part, don't bother too much, these days everything is already implemented for you, Use jQuery Tag-It: http://aehlke.github.io/tag-it/

Update

Because EntityFramework gives a hard time with mapping list of string just create a simple class for it:

public class Tag
{
    public string Name { get; set; }
}

And use

public IList<Tag> Tags { get; set; }

Again, if not for EF's limitations (or if you use nhibernate) I would still recommend mapping to a list of strings just to avoid complexity (assuming speed is not the key)

Adam Tal
  • 5,911
  • 4
  • 29
  • 49
  • 1
    +1 for Tag-It. Exactly what I need for another project. Thanks! – Rohrbs May 20 '13 at 17:03
  • THanks - to confirm, should that read: public IList Tags { get; set; }? And should that just go directly into the Place object? – niico May 20 '13 at 17:53
  • About what Chris offered for creating a Tag class, you can do almost everything with that list of strings that you can do with a dedicated class, you can select all tags of all places with a distinct and auto complete them, check for similar names, etc.. I'm not saying that you shouldn't go for a Tag class, I just think it won't have real value (the admin edits the tags so selecting them a lot of times isn't a big issue) – Adam Tal May 20 '13 at 17:57
  • OK I've added public IList Tags { get; set; } to my places class - but it's not being added to the database on update-database -verbose. (I have added a different field as a test which does work). Any idea whats going on there? – niico May 20 '13 at 19:29
  • 1
    I think it's totaly off topic. You should ask a seperate question (after some google search of course). – Adam Tal May 20 '13 at 19:53
  • It kind of appears here (unless I've msised something) - that I effectively have to crate a related model anyway to get it to persist in the database - is there a way round it? http://stackoverflow.com/questions/8317749/entity-framework-code-first-liststring-property-mapping – niico May 20 '13 at 20:57
  • OK I should have been more clear from the start that I need a solution that works well in Code First with EF5. With that in mind would a M2M relationship to a tag object, as @ChrisPratt suggests, make more sense? – niico May 20 '13 at 20:59
  • Yes, according to what I see here, you do need to map it to a different model, I'll update my answer for that because I still think you shouldn't work too hard for the UI part.. and this is why I prefer NHibernate over EF :) – Adam Tal May 20 '13 at 21:01
  • I would like to move this to another question as you say - to ask about persistence, but when I click 'ask question' it just edits this one - perhaps it wont let me ask another one so soon? – niico May 20 '13 at 21:12
  • Ive done that (Tag object has 3 properties, TagID, name & nameplural) - but it still isn't persisting. I think the problem is how it should create a many to many relationship? – niico May 20 '13 at 22:57
  • Again, off topic: ) though I don't think you need to save the plural version to the tag to the db. – Adam Tal May 21 '13 at 05:05
2

You can really handle it either way; it just depends on what you want and what you're trying to accomplish. As a comma-delimited string or an IList<string>, it would at least be searchable, but not much more than that. You'd also have a very hard time doing any sort of normalization on the tags (i.e. different places might have "Chinese Buffet", "chinese", "chinese buffet", "chinese-buffet", etc.).

The most robust method would be an actual Tag entity with a M2M relationship with Place. With that you can get a fair degree of normalization from making the name a unique column, and performing pre-emptive de-duplication queries (i.e. With entry: "chinese", "Did you mean 'Chinese'?", etc.).

Having a Tag entity also lets you easily do autocomplete queries, as you'll have one place to look up all tags or tags starting with a certain character(s). A comma-delimited string would be near impossible to implement this with as it would requiring aggregating all those strings (and across all different types of objects) first and then parsing out unique tags before finally returning a list.

UPDATE

I don't know of any existing tag related projects for MVC off the top of my head. Google should be able to help or just search in NuGet Package Manager in Visual Studio.

However, this is pretty simple to roll your own.

public class Tag
{
    public int TagId { get; set; }
    public string Name { get; set; }
    // If your tags will be more freeform (and not necessarily URL compatible)
    public string Slug { get; set; }
}

public class Place
{
    ...
    public virtual ICollection<Tag> Tags
}

The only difficult part here is that in order for EF to recognize a relationship as M2M, both sides of the relationship need to have a navigation property pointing to the other. You have two options:

  1. Go ahead and add navigation properties to both sides. So, in Tag you would add the line:

    public virtual ICollection<Place> Places
    

    There's nothing "wrong" with this and in fact may be desired if you want to be able to easily look up places for a particular tag. However, you can still get that information without a navigation property, i.e.:

    db.Places.Where(m => m.Tags.Contains(someTag))
    
  2. Use an EntityTypeConfiguration to explictly tell EF that this is an M2M. The pattern I normally follow for this is to create a "Mapping" class within my entity:

    public class Place
    {
        ...
    
        public class PlaceMapping : EntityTypeConfiguration<Place>
        {
            public PlaceMapping()
            {
                HasMany(m => m.Tags).WithMany();
            }
        }
    }
    

    Then, in your DbContext:

    public override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new Place.PlaceMapping());
        // Any other configurations added here the same way
    }
    

    This is just the pattern I follow to keep things organized, you can place your EntityTypeConfiguration wherever you like as long as you can reference it from your DbContext.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • thanks - do you know of any example projects demonstrating the use of tags? @AdamFridental – niico May 20 '13 at 17:55
  • what would be the best approach in creating a M2M relationship? I have never done this in MVC – niico May 20 '13 at 19:11
  • very helpful thanks - is there a performance hit to adding it to both sides? (not with lazyloading I guess?). Is an ICollection preferable to a List? – niico May 20 '13 at 22:59
  • There's no performance hit. EF lazy loads by default so if you don't use it, it's not queried (though you can accidentally trigger the query doing mapping with tools like AutoMapper, so if you're really watching your queries, you should make sure to tell it to ignore navigation properties you don't want to utilize). `ICollection` is what EF uses for relationships to collections of other entities. At least as of EF5, your navigation properties will not return any results unless they are typed as `ICollection`s. – Chris Pratt May 20 '13 at 23:40
  • It says 'virtual' is not allowed in: public virtual ICollection Places; - any idea why? – niico May 21 '13 at 08:17
  • doh got it - missing {get;set;} – niico May 21 '13 at 08:35