Ondrej
Ondrej

Reputation: 606

Parse MVC route argument into instance

Is it possible to implicitly convert route argument by controller method from it's string representation into instance of an object with the default Binder?

Let's say I have class BusinessObjectId that contains two properties and can be converted from/to string

public class BusinessObjectId
{
    private static readonly IDictionary<bool, char> IdTypeMap = new Dictionary<bool, char> { [false] = 'c', [true] = 'd' };
    private static readonly Regex StrIdPattern = new Regex("^(?<type>[cd]{1})(?<number>\\d+)$", RegexOptions.Compiled);

    public long Id { get; set; }
    public bool IsDraft { get; set; }

    public BusinessObjectId() { }
    public BusinessObjectId(long id, bool isDraft)
    {
        Id = id;
        IsDraft = isDraft;
    }
    public BusinessObjectId(string strId)
    {
        if (string.IsNullOrEmpty(strId)) return;
        var match = StrIdPattern.Match(strId);
        if (!match.Success) throw new ArgumentException("Argument is not in correct format", nameof(strId));
        Id = long.Parse(match.Groups["number"].Value);
        IsDraft = match.Groups["type"].Value == "d";
    }

    public override string ToString()
    {
        return $"{IdTypeMap[IsDraft]}{Id}";
    }

    public static implicit operator string(BusinessObjectId busId)
    {
        return busId.ToString();
    }

    public static implicit operator BusinessObjectId(string strBussId)
    {
        return new BusinessObjectId(strBussId);
    }
}

These actionlinks are translated into nice urls:

@Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, false) } ... url:"/sample1/c123"

@Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, true) } ... url:"/sample1/d123"

Then I'd like to use parameters in controller methods like this:

public class HomeController1 : Controller
{
    [Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
    public ActionResult Sample1(BusinessObjectId oSampleId)
    {
        // oSampleId is null
        throw new NotImplementedException();
    }

    [Route("sample2/{sSampleId:regex(^[cd]{1}\\d+$)}")]
    public ActionResult Sample2(string sSampleId)
    {
        BusinessObjectId oSampleId = sSampleId;
        // oSampleId is initialized well by implicit conversion
        throw new NotImplementedException();
    }
}

Method Sample1 doesn't recognize incoming argument and the instance oSampleId is null. In method Sample2 implicit conversion from string represetation works well, but I'd prefer not to call it manually.

Upvotes: 1

Views: 52

Answers (1)

Ondrej
Ondrej

Reputation: 606

Well I've found the answer ... You should write custom TypeConverter that can convert BusinessObjectId class from string and Decorate it with attribute

[TypeConverter(typeof(BusinessObjectIdConverter))]
public class BusinessObjectId
{ ... }

public class BusinessObjectIdConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return new BusinessObjectId((string)value);
    }
}

From now on you can use BusinessObjectId as parameter in controller methods and it will be initialized like a charm :-)

public class HomeController1 : Controller
{
    [Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
    public ActionResult Sample1(BusinessObjectId oSampleId)
    {
        // TODO: oSampleId is successfully parsed from it's string representations
    }
}

Upvotes: 1

Related Questions