Constraining string length in domain classes
Asked Answered
R

3

9

I have a persistence ignorant domain model that uses abstract repositories to load domain objects. The concrete implementation of my repositories (the data access layer (DAL)) uses entity framework to fetch data from a sql server database. The database has length constraints on a lot of its varchar columns. Now imagine that I have the following domain class:

public class Case
{
    public Case(int id, string text)
    {
         this.Id = id;
         this.Text = text;
    }

    public int Id { get; private set; }
    public string Text { get; set; }
}

And an abstract repository defined as follows:

public abstract class CaseRepository
{
    public abstract void CreateCase(Case item);
    public abstract Case GetCaseById(int id);
}

The [text] column of the table in sqlserver is defined as nvarchar(100)

Now I know that I mentioned that my domain class (Case) was persistence ignorant, nevertheless I feel that it is wrong that it allows for values of the text parameter that cannot ultimately be saved by my concrete repository implementation because the entity framework will throw an exception when assigning the text property to the entity framework generated class when it is longer than 100 characters. So I have decided that I wish to check this constraint in the domain model, because this allows me to check data validity before attempting to pass it on to the DAL, and thus making error reporting more centric to the domain object. I guess you could argue that I could just check the constraint in my constructor and in the property setter, but since I have hundreds of classes that all have similar constraints I wanted a more generic way to solve the problem

Now, the thing that I've come up with is a class called ConstrainedString, defined as follows:

public abstract class ConstrainedString
{
    private string textValue;

    public ConstrainedString(uint maxLength, string textValue)
    {
        if (textValue == null) throw new ArgumentNullException("textValue");
        if (textValue.Length > maxLength) 
            throw new ArgumentException("textValue may not be longer than maxLength", "textValue");

        this.textValue = textValue;
        this.MaxLength = maxLength;
    }

    public uint MaxLength { get; private set; }

    public string Value 
    { 
        get 
        {
            return this.textValue;
        } 

        set 
        {
            if (value == null)
                throw new ArgumentNullException("value");
            if (value.Length > this.MaxLength) throw new ArgumentException("value cannot be longer than MaxLength", "value");
            this.textValue = value;
        } 
    }
}

Furthermore I have an implementation of ConstrainedString called String100 :

public class String100 : ConstrainedString
{
    public String100(string textValue) : base(100, textValue) { }
}

Thus leading to a different implementation of Case that would look like this:

public class Case
{
    public Case(int id, String100 text)
    {
         this.Id = id;
         this.Text = text;
    }

    public int Id { get; private set; }
    public String100 Text { get; set; }
}

Now, my question is; Am I overlooking some built-in classes or some other approach that I could use instead? Or is this a reasonable approach?

Any comments and suggestions are most welcome.

Thank you in advance

Romina answered 26/1, 2010 at 11:44 Comment(3)
+1 Good question - I have often wondered why such types don't exist in the BCL. However, I think it smells of a Leaky Abstraction that you are letting the relational database influence the Domain Model like that, or is there a compelling Domain reason for these constraints?Gap
@Mark Seemann I agree with you that it smells, and no, for the most parts there is no compelling domain reason for the constraints. Basically it's a legacy database schema that could probably benefit from some refactoring, but that is outside our current scope. But nevertheless I don't think that I can ignore these constraints in the model because I want to make data validation a part of the model and NOT a part of the repository implementation (which I think is a fair ambition)Romina
Ah, the wonderful world of legacy apps... Do consider making a Decorator layer for your Domain Model that implements these constraints to keep your Domain Model clean. See here for a bit more details: community.ative.dk/blogs/ative/archive/2006/09/29/Migration-2D00-Bug_2D00_by_2D00_Bug-Compatibility.aspxGap
M
1

I believe your validation should reside in your domain model. The constraints on your fields directly represent some business logic. Ultimately you have to validate before you persist anyway.

Miksen answered 17/2, 2010 at 3:14 Comment(0)
P
0

I think this depends on many factors (as well as some personal preferences). Sometimes the constraint should form part of the domain object - for example with social security numbers/passport numbers... - these normally have a fixed length and cannot vary as a domain rule - not a data persistence rule (although you might constrain the db as well).

Some prefer to not have these sort of checks in their domain model and instead have something like a validation attribute on the property that can be inspected and executed external from the domain object by a seperate validator.

The issue you might have with your method (although not difficult to get around) is getting any ORM/Mapper - if you're using one - to know how to map a string to/from the db to your ConstrainedString.

The ConstrainedString might not get around the issue of the domain object having extra info about the constraint as it might need to construct the ConstrainedString

Phalanx answered 26/1, 2010 at 12:6 Comment(0)
P
0

If you change the constraints of a Case, it makes sense that you'd have to make a new one - you've changed the contract, and old code will no longer know if it's meeting the requirements or not.

Instead of worrying about what your repository will or will not allow, define what you will allow in your class, and make sure that you find a way to work with any repository that you change to in the future. You own your API - your dependencies do not.

Pooh answered 17/2, 2010 at 3:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.