Skip to main content

.NET Data Contracts

Introduction

Data Contracts are a fundamental concept in .NET service development that provide a formal agreement between a service and its clients about how data is structured and exchanged. They serve as the bridge that allows different applications, potentially written in different programming languages or running on different platforms, to communicate effectively with each other.

In this tutorial, we'll explore what data contracts are, how they work, and how to implement them correctly in your .NET applications.

What are Data Contracts?

A Data Contract is a formal agreement between a service and a client that abstractly describes the data to be exchanged. In .NET, data contracts are implemented using classes or structures decorated with the [DataContract] attribute, with members marked with the [DataMember] attribute to specify which fields or properties should be included in the serialization process.

Data contracts are especially important in service-oriented architectures, where they help ensure:

  • Interoperability: Systems built on different platforms can communicate
  • Versioning: Services can evolve while maintaining backward compatibility
  • Validation: Data can be validated against an expected structure
  • Abstraction: Implementation details can be hidden from external clients

Creating Basic Data Contracts

Let's start by creating a simple data contract:

csharp
using System;
using System.Runtime.Serialization;

[DataContract]
public class Customer
{
[DataMember]
public int Id { get; set; }

[DataMember]
public string Name { get; set; }

[DataMember]
public string Email { get; set; }

// This property won't be serialized because it's not marked with [DataMember]
public DateTime LastLoggedIn { get; set; }
}

When this class is serialized (converted to XML, JSON, or another format for transmission), only the Id, Name, and Email properties will be included. The LastLoggedIn property will be ignored because it's not part of the data contract.

Using Data Contracts in a Service

Here's how you might use the above data contract in a simple WCF service:

csharp
[ServiceContract]
public interface ICustomerService
{
[OperationContract]
Customer GetCustomer(int id);

[OperationContract]
void SaveCustomer(Customer customer);
}

public class CustomerService : ICustomerService
{
public Customer GetCustomer(int id)
{
// In a real application, we would fetch from a database
return new Customer
{
Id = id,
Name = "John Doe",
Email = "[email protected]",
LastLoggedIn = DateTime.Now // This won't be serialized
};
}

public void SaveCustomer(Customer customer)
{
// Save customer to database
Console.WriteLine($"Saved customer: {customer.Name}");
}
}

Advanced Data Contract Features

Setting Names and Namespaces

You can customize how properties appear in the serialized output:

csharp
[DataContract(Name = "ClientInfo", Namespace = "http://mycompany.com/schemas")]
public class Customer
{
[DataMember(Name = "ClientId")]
public int Id { get; set; }

[DataMember(Name = "ClientName")]
public string Name { get; set; }
}

When serialized to XML, this will produce:

xml
<ClientInfo xmlns="http://mycompany.com/schemas">
<ClientId>1</ClientId>
<ClientName>John Doe</ClientName>
</ClientInfo>

Controlling Order

You can control the order in which members appear in the serialized output:

csharp
[DataContract]
public class Person
{
[DataMember(Order = 2)]
public string FirstName { get; set; }

[DataMember(Order = 1)]
public string LastName { get; set; }
}

In this example, LastName will appear before FirstName in the serialized data, regardless of how they're defined in the class.

Required Members

You can mark data members as required to ensure they're always included:

csharp
[DataContract]
public class Order
{
[DataMember(IsRequired = true)]
public int OrderId { get; set; }

[DataMember]
public DateTime OrderDate { get; set; }
}

If a required member is missing during deserialization, a SerializationException will be thrown.

Handling Collections

Collections are handled naturally in data contracts:

csharp
[DataContract]
public class ShoppingCart
{
[DataMember]
public int CustomerId { get; set; }

[DataMember]
public List<OrderItem> Items { get; set; }
}

[DataContract]
public class OrderItem
{
[DataMember]
public int ProductId { get; set; }

[DataMember]
public int Quantity { get; set; }

[DataMember]
public decimal UnitPrice { get; set; }
}

Data Contract Versioning

One of the most powerful features of data contracts is their ability to handle versioning gracefully.

Adding New Members

You can add new members to a data contract without breaking existing clients:

csharp
// Version 1
[DataContract]
public class Product
{
[DataMember]
public int Id { get; set; }

[DataMember]
public string Name { get; set; }
}

// Version 2
[DataContract]
public class Product
{
[DataMember]
public int Id { get; set; }

[DataMember]
public string Name { get; set; }

[DataMember]
public decimal Price { get; set; } // New member
}

Older clients that don't know about the Price property will simply ignore it, while newer clients can take advantage of it.

Handling Removed Members

If you need to remove members, you should mark them as obsolete first rather than deleting them outright:

csharp
[DataContract]
public class Customer
{
[DataMember]
public int Id { get; set; }

[DataMember]
public string FullName { get; set; }

[DataMember]
[Obsolete("Use FullName instead")]
public string Name { get; set; }
}

Using Data Contract Resolvers

For more complex versioning scenarios, you can implement custom DataContractResolver classes:

csharp
public class VersioningDataContractResolver : DataContractResolver
{
public override bool TryResolveType(
Type type, Type declaredType, DataContractResolver knownTypeResolver,
out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)
{
// Custom logic for resolving types
if (type == typeof(NewCustomerType) && declaredType == typeof(OldCustomerType))
{
XmlDictionary dictionary = new XmlDictionary();
typeName = dictionary.Add("OldCustomer");
typeNamespace = dictionary.Add("http://mycompany.com/schemas");
return true;
}

return knownTypeResolver.TryResolveType(
type, declaredType, null, out typeName, out typeNamespace);
}

public override Type ResolveName(
string typeName, string typeNamespace, Type declaredType,
DataContractResolver knownTypeResolver)
{
// Custom logic for resolving names
if (typeName == "OldCustomer" &&
typeNamespace == "http://mycompany.com/schemas" &&
declaredType == typeof(OldCustomerType))
{
return typeof(NewCustomerType);
}

return knownTypeResolver.ResolveName(
typeName, typeNamespace, declaredType, null);
}
}

Real-World Example: RESTful API with Data Contracts

Let's build a more practical example of how data contracts can be used in a modern RESTful API using ASP.NET Core:

csharp
// Data Contracts
[DataContract]
public class ProductDto
{
[DataMember(Name = "productId")]
public int Id { get; set; }

[DataMember(Name = "productName")]
public string Name { get; set; }

[DataMember(Name = "unitPrice")]
public decimal Price { get; set; }

[DataMember(Name = "inStock")]
public bool IsAvailable { get; set; }
}

[DataContract]
public class ProductCreationDto
{
[DataMember(IsRequired = true)]
public string Name { get; set; }

[DataMember(IsRequired = true)]
public decimal Price { get; set; }

[DataMember]
public bool IsAvailable { get; set; } = true;
}

// Controller using the data contracts
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static List<ProductDto> _products = new List<ProductDto>
{
new ProductDto { Id = 1, Name = "Laptop", Price = 999.99m, IsAvailable = true },
new ProductDto { Id = 2, Name = "Mouse", Price = 24.99m, IsAvailable = true }
};

[HttpGet]
public ActionResult<IEnumerable<ProductDto>> GetProducts()
{
return Ok(_products);
}

[HttpGet("{id}")]
public ActionResult<ProductDto> GetProduct(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null)
return NotFound();

return Ok(product);
}

[HttpPost]
public ActionResult<ProductDto> CreateProduct(ProductCreationDto productDto)
{
var newProduct = new ProductDto
{
Id = _products.Max(p => p.Id) + 1,
Name = productDto.Name,
Price = productDto.Price,
IsAvailable = productDto.IsAvailable
};

_products.Add(newProduct);

return CreatedAtAction(
nameof(GetProduct),
new { id = newProduct.Id },
newProduct
);
}
}

Best Practices for Data Contracts

  1. Keep Data Contracts Simple: Design data contracts to transfer data, not behavior.

  2. Use Appropriate Names: Name your data contracts to reflect their role (e.g., CustomerDto instead of just Customer).

  3. Consider Versioning from the Start: Plan for how your contracts might evolve over time.

  4. Validate Data: Even though data contracts help ensure structure, always validate data on both client and server sides.

  5. Use Separate Contracts for Input and Output: Often, what you receive from a client and what you send back are different.

  6. Document Your Contracts: Good documentation makes integration easier for consumers of your service.

  7. Don't Expose Implementation Details: Keep your internal domain models separate from your data contracts.

  8. Consider Security: Don't include sensitive information in data contracts unless necessary.

Common Pitfalls

  • Circular References: Serialization can fail with circular references. Use the [DataMember(EmitDefaultValue = false)] attribute to help with this.

  • Performance Issues: Overly complex data contracts can lead to serialization performance problems.

  • Breaking Changes: Removing or renaming data members can break clients. Add new members instead and mark old ones as obsolete.

  • Ignoring Private Fields: By default, private fields aren't serialized. Use the [DataMember] attribute explicitly if you need to include them.

Summary

Data contracts are a powerful mechanism in .NET for defining how data is exchanged between services and clients. They provide a clear, formal way to specify the structure of data, enabling interoperability and supporting versioning as your applications evolve.

In this tutorial, we've covered:

  • The basics of creating and using data contracts
  • Advanced features like custom naming, order, and required members
  • How to handle versioning of data contracts
  • A practical example of using data contracts in a RESTful API
  • Best practices and common pitfalls

By following these principles, you can build robust, maintainable services that communicate effectively with clients across different platforms and technologies.

Additional Resources

Exercises

  1. Create a basic Order data contract with associated OrderItem contracts, and implement a simple service that allows creating and retrieving orders.

  2. Extend your data contracts to handle versioning by adding new fields while maintaining backward compatibility.

  3. Implement a REST API using ASP.NET Core that uses separate data contracts for input and output operations.

  4. Create a client application that consumes the service you built, demonstrating how data contracts work across application boundaries.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)