1

I am trying to create the following constraint in my model so that a Tag object's TagType is valid. A valid TagType is one whose OperatingCompanyId matches the Tag's Website's OperatingCompanyId. I realize that this seems convoluted however it makes sense from a business standpoint:

An Operating Company has WebSites. Websites contain Tags. Tags have a TagType(singular). TagTypes are the same across Operating Companies, meaning that if one Operating Company has twenty TagTypes and five WebSites, those twenty TagTypes should be able to be used across all fives of those WebSites. I want to ensure that a Tag's TagType cannot be one associated with another OperatingCompany.

What is the best way to create this constraint in the model? Do I need to change my POCO, or use the Fluent API?

Thanks in advance!

[Table("OperatingCompanies")]
public class OperatingCompany : ConfigObject
{
    public OperatingCompany()
    {
        WebSites = new List<WebSite>();
    }

    [Required(ErrorMessage = "Name is a required field for an operating company.")]
    [MaxLength(100, ErrorMessage = "Name cannot exceed 100 characters.")]
    public string Name { get; set; }

    public virtual ICollection<WebSite> WebSites { get; set; }
}

[Table("Websites")]
public class WebSite : ConfigObject
{
    public WebSite()
    {
        WebObjects = new List<WebObject>();
    }

    [Required(ErrorMessage = "URL is a required field for a web site.")]
    [MaxLength(100, ErrorMessage = "URL cannot exceed 100 characters for a web site.")]
    [RegularExpression(@"\b(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|]", ErrorMessage = "The value entered is not a valid URL.")]
    public string Url { get; set; }

    public OperatingCompany OperatingCompany { get; set; }

    [Required(ErrorMessage = "You must associate a web site with an operating company.")]
    public Guid OperatingCompanyId { get; set; }

    [InverseProperty("Website")]
    public virtual ICollection<WebObject> WebObjects { get; set; }
}

[Table("Tags")]
public class Tag : ConfigObject
{
    [Required(ErrorMessage = "Name is a required field for a tag.")]
    [MaxLength(100, ErrorMessage = "Name cannot exceed 100 characters for a tag.")]
    public string Name { get; set; }

    public TagType TagType { get; set; }

    [Required(ErrorMessage = "You must associate a tag with a tag type.")]
    public Guid TagTypeId { get; set; }

    public WebSite WebSite { get; set; }

    [Required(ErrorMessage = "You must associate a tag with a web site.")]
    public Guid WebSiteId { get; set; }
}

[Table("TagTypes")]
public class TagType : ConfigObject
{
    [Required(ErrorMessage = "Name is a required field for a tag.")]
    [MaxLength(100, ErrorMessage = "Name cannot exceed 100 characters for a tag type.")]
    public string Name { get; set; }

    public OperatingCompany OperatingCompany { get; set; }

    [Required(ErrorMessage = "You must associate a tag type with an operating company.")]
    public Guid OperatingCompanyId { get; set; }
}
DMC
  • 361
  • 4
  • 15

3 Answers3

2

however... if I understand the purpose of MVC / EF it is to have that business logic inside of the Model...

And what model do you mean? If you take ASP.NET MVC and EF you will end with three areas which are sometimes called model:

  • EF model - that is set of classes with their mapping to database
  • Model-View-Controller - model here means something (usually business logic) consumed by your controller to prepare data for view
  • View model - In ASP.NET MVC view model is class with data exchanged between controller and view

If I look at your classes I see first and third model coupled together (most of the time this is considered as a bad practice). Your understanding is correct but mostly in terms of second model which is not represented by your classes. Not every "business logic" can be represented by mapping. Moreover it is not a point of data layer to do business logic.

Your mapping partially works (tag type is related only to one operating company) but still your data layer doesn't enforce all your business rules. Data layer still allows web site to have assigned tag with tag type from different operating company and your business logic must ensure that this will not happen. Avoiding this in database would be complicated because it would probably require complex primary keys and passing operating company Id to every dependent object.

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • what exactly about my classes is 'bad practice'? i realize that this model currently allows tags to have tag types from different operating companies, and while it may be complicated this is something that can definitely be constrained by foreign keys in the database; i just can't figure out how to get CF to pick up on that through the fluent API yet. – DMC Jul 18 '11 at 13:07
  • There is plenty of people on this web who think that messing entity model with UI validation is bad practice. – Ladislav Mrnka Jul 18 '11 at 21:43
  • I'm sure there are. But you are not explaining why and you are not answering my question. – DMC Jul 19 '11 at 12:14
  • Why? Read about separation of concerns. Normally you should not mess mapping and validation in single class. It can be OK for very simple and small projects but once you do anything more complicated you will find quite soon that it doesn't work because your individual UI screens can demand slightly different validation logic. – Ladislav Mrnka Jul 19 '11 at 12:25
  • You didn't answer my question though - you're talking about separation of concerns which, even if I made your suggested changes, does not address my original question: how does one create this constraint in MVC3? – DMC Aug 17 '11 at 18:40
  • No I didn't. I just made a code review of your code to point you to some design flaws you did. @Morteza has answered your question nicely. – Ladislav Mrnka Aug 17 '11 at 19:14
2

One way to enforce this constraint is to take advantage of the new validation feature introduced as part of new DbContext API in EF 4.1. You can write a custom validation rule to make sure that tag types for any given company's website are selected from the valid tag types for that company. The following shows how it can be done:

public abstract class ConfigObject
{
    public Guid Id { get; set; }
}

public class OperatingCompany : ConfigObject, IValidatableObject
{
    public string Name { get; set; }

    public virtual ICollection<WebSite> WebSites { get; set; }
    public virtual List<TagType> TagTypes { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var allTagTypes = (from w in WebSites from t in w.Tags select t.TagType);

        if (!allTagTypes.All(wtt => TagTypes.Exists(tt => tt.Id == wtt.Id)))
        {
            yield return new ValidationResult("One or more of the website's tag types don't belong to this company");
        }            
    }
}

public class WebSite : ConfigObject
{
    public string Url { get; set; }                
    public Guid OperatingCompanyId { get; set; }

    public virtual ICollection<Tag> Tags { get; set; }
    public OperatingCompany OperatingCompany { get; set; }                
}

public class Tag : ConfigObject
{
    public string Name { get; set; }
    public Guid TagTypeId { get; set; }
    public Guid WebSiteId { get; set; } 

    public TagType TagType { get; set; }               
    public WebSite WebSite { get; set; }
}

public class TagType : ConfigObject
{
    public string Name { get; set; }
    public Guid OperatingCompanyId { get; set; }

    public OperatingCompany OperatingCompany { get; set; }                
}

public class Context : DbContext
{
    public DbSet<OperatingCompany> OperatingCompanies { get; set; }
    public DbSet<WebSite> WebSites { get; set; }
    public DbSet<Tag> Tags { get; set; }
    public DbSet<TagType> TagTypes { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Tag>().HasRequired(t => t.WebSite)
                                  .WithMany(w => w.Tags)
                                  .HasForeignKey(t => t.WebSiteId)
                                  .WillCascadeOnDelete(false);
    }
}

As a result, EF will invoke that validate method each time you call DbContext.SaveChanges() to save an OperatingCompany object into database and EF will throw (and abort the transaction) if the method yields back any validation error. You can also proactively check for validation errors by calling the GetValidationErrors method on the DbContext class to retrieve a list of validation errors within the model objects you are working with.

It also worth noting that since you use your domain model as also a View Model for your MVC layer, MVC will recognize and honor this Validation rule and you can check for the validation result by looking into the ModelState in the controller. So it really get checked in two places, once in your presentation layer by MVC and once in the back end by EF.

Hope this helps.

Morteza Manavi
  • 33,026
  • 6
  • 100
  • 83
  • 1
    This is *EXACTLY* what I was after - thank you SO much Morteza! FYI - anyone who sees this... if you have other questions regarding Entity Framework / Code First, I strongly suggest checking out Morteza Manavi's blog as he has lots of great examples and explanations here that make a lot of sense: http://weblogs.asp.net/manavi – DMC Jul 19 '11 at 12:17
0

If I were you, I will use business layer to filter Tagtype instead of do such constraint in database. For me that approach may be easier.

John Liu
  • 186
  • 4
  • Thanks for the response John, however... if I understand the purpose of MVC / EF it is to have that business logic inside of the Model... I will definitely add code to the View to filter, but I want to enforce the constraint in the Model. Does that make sense? – DMC Jul 15 '11 at 18:37