1
\$\begingroup\$

What do you think about this approach to validate and analyze URLs? There are Url and Url<TResource> model types to represent generic and specific site URL respectively. Specific site URLs can be validated against specific schema and provide custom data extraction API.

Let's imagine the following DTO and EF Entity Type classes:

class DtoPresentation
{
   public int Id { get; set; }
   public Url Transcript { get; set; } 
   public Url<YouTube> Video { get; set; }
}

class EPresentation
{
   public int Id { get; set; }
   public string Transcript { get; set; } 
   public string Video { get; set; }
}

Reader/Writer look this way:

class PresentationReader : IPresentationReader  
{
    public DtoPresentation Read(int id)   
    {
       EPresentation e = ...
       return new DtoPresentation
       {
           Id = e.Id,
           Transcript = (UnsafeUrlCast)e.Transcript,
           Video = (UnsafeUrlCast)e.Video
       }
   } 
}

class PresentationWriter : IPresentationWriter  
{
   public void Write(DtoPresentation dto)   {
       var e = new EPresentation
       {
           Id = dto.Id,
           Transcript = dto.Transcript,
           Video = dto.Video
       }

       ...   
   } 
}

Can you see this mapping between String and Url or Url<T>? There are many ways to copy value for a given:

string urlString;
Url url;    

Copy from String to Url - validation are done here (probably we need an explicit cast here as it can throw...):

url = urlString;

Copy from String to Url without validation - always succeeds:

url = (UnsafeUrlCast)urlString; 

Copy from Url to String - always succeeds:

urlString = url;

Generic Url model:

[TypeConverter(typeof(UrlTypeConverter))]
[JsonConverter(typeof(UrlJsonConverter))]
public struct Url
{
    public static implicit operator Url(UnsafeUrlCast cast) => 
        new Url(cast.ToString(), false);
    public static explicit operator Url(string text) => 
        new Url(text, true);
    public static implicit operator string(Url url) => 
        url.Text;

    public Url(string text, bool validate)
        : this()
    {
        if (String.IsNullOrWhiteSpace(text))
            return;

        Text = text;
        if(validate)
            new WebResource()
                .ValidateUrl(this);
    }

    public string Text { get; }
    public bool Empty => Text == null;
    public override string ToString() => Text;
    public override bool Equals(object obj) => Text == obj?.ToString();
    public override int GetHashCode() => Text?.GetHashCode() ?? 0;
}

Specific site Url model:

[TypeConverter(typeof(UrlTypeConverter))]
[JsonConverter(typeof(UrlJsonConverter))]
public struct Url<T> where T : WebResource, new()
{
    public static implicit operator Url<T>(UnsafeUrlCast cast) => 
        new Url<T>(cast.ToString(), false);
    public static explicit operator Url<T>(string text) => 
        new Url<T>(text, true);
    public static implicit operator string(Url<T> url) => 
        url.Text;
    public static explicit operator Url<T>(Url url) => 
        new Url<T>(url, true);
    public static implicit operator Url(Url<T> url) => 
        new Url(url, true);

    public Url(string text, bool validate)
        : this()
    {
        if (String.IsNullOrWhiteSpace(text))
            return;

        Text = text;
        if(validate)
            new T()
                .ValidateUrl(this);
    }

    public string Text { get; }
    public bool Empty => Text == null;
    public override string ToString() => Text;
    public override bool Equals(object obj) => Text == obj?.ToString();
    public override int GetHashCode() => Text?.GetHashCode() ?? 0;
}

Unsafe cast helper:

public struct UnsafeUrlCast
{
    public static explicit operator UnsafeUrlCast(string text) => new UnsafeUrlCast(text);

    UnsafeUrlCast(string text)
    {
        Text = text;
    }

    string Text { get; }
    public override string ToString() => Text;
}

Where WebResource is:

public class WebResource
{
    public virtual void ValidateUrl(string text)
    {
        string pattern = 
            @"^(http:\/\/|https:\/\/)[-a-zA-Z0-9+&@#\/%=~_|$?!:;,.\*\(\)]*[a-zA-Z0-9+&@#\/%=~\-_|$]$";
        if (!Regex.IsMatch(text, pattern))
            throw new FormatException($"Invalid URL: {text}.");

        if (text.Length > 2048)
            throw new FormatException($"URL {text} exceeds 2048 characters.");
    }
}

public class YouTubeVideo : WebResource
{        
}

public class YouTubeThumb : WebResource
{
}

Here is how we have custom URL data extraction:

public static class YouTube
{
    public static Url<YouTubeThumb> ToThumb(this Url<YouTubeVideo> video)
    {
        return (Url<YouTubeThumb>)ThumbUrl(video);
    }

    static string ThumbUrl(string videoUrl)
    {
        var id = Id(videoUrl);
        if (id == null)
            return null;

        return $"https://img.youtube.com/vi/{id}/mqdefault.jpg";
    }

    static string Id(string videoUrl)
    {
        if (videoUrl == null)
            return null;

        string pattern = 
            ".*(?:youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?.*v=)([^#\\&\\?]*).*";
        var match = Regex.Match(videoUrl, pattern);
        if (!match.Success)
            return null;

        return match.Groups[1].ToString();
    }
}

So we can:

Url<YouTubeVideo> video = ...
Url<YouTubeThumb> thumb = video.ToThumb();

And to make these type properly work in ASP.NET model binding:

class UrlTypeConverter : TypeConverter
{
    public UrlTypeConverter(Type type)
    {
        Type = type;
    }

    Type Type { get; }

    public override bool CanConvertFrom(
        ITypeDescriptorContext context, Type sourceType) =>
            sourceType == typeof(string) ||
            base.CanConvertFrom(context, sourceType);

    public override object ConvertFrom(
        ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value == null || value is string)
            return Activator.CreateInstance(Type, value, true);

        return base.ConvertFrom(context, culture, value);
    }

    public override bool CanConvertTo(
        ITypeDescriptorContext context, Type destinationType) =>
            destinationType == typeof(string) ||
            base.CanConvertTo(context, destinationType);

    public override object ConvertTo(
         ITypeDescriptorContext context, CultureInfo culture, 
         object value, Type destinationType)
    {
        if (destinationType == typeof(string))
            return value?.ToString();

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

And JSON serialization:

class UrlJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) =>
        objectType == typeof(Url) || 
        (objectType.IsConstructedGenericType &&
         objectType.GetGenericTypeDefinition() == typeof(Url<>));

    public override object ReadJson(
        JsonReader reader, 
        Type objectType, 
        object existingValue, 
        JsonSerializer serializer) =>
           Activator.CreateInstance(objectType, reader.ReadAsString(), true);

    public override void WriteJson(
         JsonWriter writer, 
         object value, 
         JsonSerializer serializer) =>
         writer.WriteValue(value.ToString());
}
\$\endgroup\$
  • \$\begingroup\$ I'm trying to understand it but I think you'll need to help me a bit... why are you mapping properties between the dto and the entity? why do you need the dto? why can't you validate/check the url before you send it to the database? \$\endgroup\$ – t3chb0t Sep 4 '16 at 15:58
  • \$\begingroup\$ @t3chb0t Well, I have URL in so many places, so I would like to have a self validating data type. It is more efforts to design it, but, the same time, a way easy to consume in many places. \$\endgroup\$ – Dmitry Nogin Sep 4 '16 at 16:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.