Create a WCF DataService in Visual Studio 2015

Datetime:2016-08-23 04:35:14          Topic: WCF  Visual Studio           Share

Introduction

There are plenty of articles available on creating web services, however it was my experience that some of the information was slightly out of date (using older tools/frameworks) and it was also my experience that I had to piece information from many sources together to get around bugs or implement all features. This article is an attempt to make a definitive guide for making a WCF Data Service in WCF using entity framework 6 and WCF Data Services 5.6. In my next article, I will show how to consume data from the service in a universal Windows platform app.

WCF Data Services (formerly known as "ADO.NET Data Services") is a component of the .NET Framework that enables you to create services that use the Open Data Protocol (OData) to expose and consume data over the Web or intranet by using the semantics of representational state transfer (REST) . OData exposes data as resources that are addressable by URIs. Data is accessed and changed by using standard HTTP verbs of GET , PUT , POST , and DELETE . OData uses the entity-relationship conventions of the Entity Data Model to expose resources as sets of entities that are related by associations.

WCF Data Services uses the OData protocol for addressing and updating resources. In this way, you can access these services from any client that supports OData. OData enables you to request and write data to resources by using well-known transfer formats: Atom, a set of standards for exchanging and updating data as XML, and JavaScript Object Notation (JSON), a text-based data exchange format used extensively in AJAX application.

WCF Data Services can expose data that originates from various sources as OData feeds. Visual Studio tools make it easier for you to create an OData-based service by using an ADO.NET Entity Framework data model. You can also create OData feeds based on common language runtime (CLR) classes and even late-bound or un-typed data.

WCF Data Services also includes a set of client libraries, one for general .NET Framework client applications and another specifically for Silverlight-based applications. These client libraries provide an object-based programming model when you access an OData feed from environments such as the .NET Framework and Silverlight.

Background

Some great articles on odata and WCF are:

Some principles on OData:

Some good information on authentication:

Information on filtering data (query interception):

Some workarounds for errors:

Information on stored procedure entities with multiple result sets:

Creating the Service

  1. Go to File -> New Project .
  2. In the list of Installed Templates, select the Visual C# | WCF tree node and then select the WCF Service Application .

  3. Delete IService1.cs and Service1.svc from the resulting project.

  4. Add a new item to the project. In the list of Installed Templates, select the Visual C# | Data tree node and then select the ADO.NET Entity Data Model .

  5. For the purposes of this article, I am selecting to build my entity module from an existing database.

  6. Define your connection. Typically a cloud server.

  7. Save your connection to the web.config file.

  8. Specify entity version 6.0.

  9. Select the tables/view/procs to include in your entity model.

  10. Add new item. In the list of Installed Templates, select the Visual C# tree node and then select the WCF Data Service .

  11. Add the entity framework provider for OData via nugget command line.
    PM> Install-Package Microsoft.OData.EntityFrameworkProvider -Pre

  12. Edit the WcfDataService1.svc

    file and:

    • Add the using statement for System.Data.Services.Providers
    • Change inheritance from DataService to EntityFrameworkDataService
    • Change the <T> to be the name of your entity model in my case futaTillHOEntities

    Optionally:

    • Set the access rules for your entity names. Use the star symbol to mean all entities.
    • Set the UseVerboseErrors property in order to see proper feedback of errors.
    using System.Data.Services.Providers;
    
    namespace WcfService1
    {
        public class WcfDataService1 : EntityFrameworkDataService<FutaTillHOEntities>
        {
            // This method is called only once to initialize service-wide policies.
            public static void InitializeService(DataServiceConfiguration config)
            {
                // TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
                // Examples:
                config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
                // config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
                config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
    	    config.UseVerboseErrors = true;
            }
        }
    }
  13. Create a new web app either from Visual Studio server explorer or your Azure portal. Then right click your project and click publish. In the publish screen, select Azure Web Apps.

  14. Select your web app that you want to publish too. In my case, the name is UtilitiesDataService .

  15. Click Next if settings are correct.

  16. Click Next if settings are correct.

  17. Click Publish.

  18. You should now be able to test your web-service using the URL of your web-app (visible in your azure portal) + the service name + the entity name

    Example:

To retrieve a specific record by including the primary key in the URL:

If the key is composite, then use the following notation:

By default, the information will be serialized as an atom feed but you can include the format specifier to get data in json:

You can also include functions such as top/skip/expand and combine them using & character:

Look at http://www.odata.org/ for more examples.

Making Views Updatable

By default, the entityset is setup in such a way to prevent updates on views. If your view is updatable, then perform the following steps:

  1. Right click the model element and select open with.

  2. Select the XML editor and click OK.

  3. Do a find for the text <DefiningQuery> . You will notice that this element and inner query is present for your views, but not for your tables.

  4. Remove the DefiningQuery element. In this screen, I’ve shown the element removed from the Behaviours view, however you will need to remove it from all of the views.

  5. Strangely, you also need to change the text store:Schema="dbo" to be just Schema="dbo" . Basically, if you look at how tables are defined, you can see the difference.

    [Table Definition]

    <EntitySet Name="Tenders" EntityType="Self.Tenders" 
    	Schema="dbo" store:Type="Tables" />

    [Original View Definition]

    <EntitySet Name="Behaviours" EntityType="Self.Behaviours" 
    store:Type="Views" store:Schema="dbo"/>

    [Corrected View Definition]

    <EntitySet Name="Behaviours" 
    	EntityType="Self.Behaviours" store:Type="Views" Schema="dbo"/>
  6. After editing and saving the XML, it is sometimes necessary to go back into the entity model designer and click the save button in the tool bar.

Basic Authentication

In order to setup basic authentication, you need two additional classes in your service. I have attached the entire source for these classes to the article.

The BasicAuthenticationModule class sets up the event handling for an authentication request and forwards to the BasicAuthenticationProvider.Authenticate method.

public class BasicAuthenticationModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            //Attach handling for authentication requests.
            context.AuthenticateRequest
               += new EventHandler(context_AuthenticateRequest);
        }
        void context_AuthenticateRequest(object sender, EventArgs e)
        {
            //Unbox the application.
            HttpApplication application = (HttpApplication)sender;

            //Send to provider for authentication.
            if (!BasicAuthenticationProvider.Authenticate(application.Context))
            {
                application.Context.Response.Status = "401 Unauthorized";
                application.Context.Response.StatusCode = 401;
                application.Context.Response.AddHeader("WWW-Authenticate", "Basic");
                application.CompleteRequest();
            }
        }
        public void Dispose() { }
    }

Here is the BasicAuthenticationProvider class. Please note that Basic Authentication in itself is not secure since the username and password are sent unencrypted. Therefore, basic authentication should only be allowed in an SSL environment. The code that enforces this condition has been commented out, in the code sample below, to allow testing.

public class BasicAuthenticationProvider
{
    /// <summary>
    /// Authenticate and the set the current http context user.
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public static bool Authenticate(HttpContext context)
    {
        //This needs to be uncommented for live site.
        //This will reject the login when not using SSL.
        //if (!HttpContext.Current.Request.IsSecureConnection)
        //    return false;
        //I only want to execute code for authorization requests.
        if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
            return false;

        string authHeader = HttpContext.Current.Request.Headers["Authorization"];

        IPrincipal principal;
        if (TryGetPrincipal(authHeader, out principal))
        {
            HttpContext.Current.User = principal;
            return true;
        }
        return false;
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="authHeader"></param>
    /// <param name="principal"></param>
    /// <returns></returns>
    private static bool TryGetPrincipal(string authHeader, out IPrincipal principal)
    {
        var creds = ParseAuthHeader(authHeader);
        if (creds != null && TryGetPrincipal(creds, out principal))
            return true;

        principal = null;
        return false;
    }

In basic authentication, the user name and password are stored in the header. This method extracts the username and password credentials into an array.

/// <summary>
        /// In basic authentication the user name and password are in the auth header in base64 encoding.
        /// </summary>
        /// <param name="authHeader"></param>
        /// <returns>The array of credentials i.e. the username and password.</returns>
        private static string[] ParseAuthHeader(string authHeader)
        {
            // Check this is a Basic Auth header
            if (
                authHeader == null ||
                authHeader.Length == 0 ||
                !authHeader.StartsWith("Basic")
            ) return null;

            // Pull out the Credentials with are separated by ':' and Base64 encoded
            string base64Credentials = authHeader.Substring(6);
            string[] credentials = Encoding.ASCII.GetString(
                  Convert.FromBase64String(base64Credentials)
            ).Split(new char[] { ':' }); 
            
            if (credentials.Length != 2 ||
                string.IsNullOrEmpty(credentials[0]) ||
                string.IsNullOrEmpty(credentials[0])
            ) return null;

            // Okay this is the credentials
            return credentials;
        }
}

It is in this overload of TryGetPrincipal that you must hook up your username/password data store. In the code sample below, I've used another entity model with a single stored procedure called aspnet_GetUserCredentials to return multiple result sets containing the login information for the supplied username. The first result set returns the username and password details, the second contains the roles for the user and the third contains the role permissions. The supplied password (from the authentication header) is hashed and compared with the stored hash. If there is a match, then the principle object is initialized to a new container for the user credentials, roles and associated permissions.

/// <summary>
///
/// </summary>
/// <param name="creds"></param>
/// <param name="principal"></param>
/// <returns></returns>
private static bool TryGetPrincipal(string[] creds, out IPrincipal principal)
{
    bool located = false;
    principal = null;

    //The user match.
    var user = new User_SprocResult();

    //The list of roles.
    var roles = new List<Role_SprocResult>();

    //The list of permissions.
    var permissions = new List<Permission_SprocResult>();

    //Use the entity context.
    using (var dbContext = new AuthenticationEntities())
    {
        //Get first enumerate result set.
        var result = dbContext.aspnet_GetUserCredentials("Utilities", creds[0]);
        user = result.FirstOrDefault();

        //Get second result set
        var result2 = result.GetNextResult<Role_SprocResult>();
        roles.AddRange(result2);

        //Get third result set
        permissions.AddRange(result2.GetNextResult<Permission_SprocResult>());
    }

    //If there are any user matches.
    if (user != null)
    {
        //Get the hash of this users password using the salt provided.
        byte[] bytes = Encoding.Unicode.GetBytes(creds[1]);
        byte[] src = Convert.FromBase64String(user.PasswordSalt);
        byte[] dst = new byte[src.Length + bytes.Length];
        Buffer.BlockCopy(src, 0, dst, 0, src.Length);
        Buffer.BlockCopy(bytes, 0, dst, src.Length, bytes.Length);
        HashAlgorithm algorithm = HashAlgorithm.Create("SHA1");
        byte[] inArray = algorithm.ComputeHash(dst);

        //If the resulting hash is equal to the stored hash for this user.
        if (string.Compare(Convert.ToBase64String(inArray), user.Password) == 0)
        {
            //Tag as located.
            located = true;

            //Set new principal.
            principal = new CustomPrincipal(user.UserName,
                roles.Select(r=>r.RoleName).ToArray(),
                permissions.Select(r=>r.PermissionId).ToArray());
        }
    }

    //Return result.
    return located;
}

Here is the specialized container for the user principal.

public class CustomPrincipal : IPrincipal
       {
           string[] _roles;
           string[] _permissions;
           IIdentity _identity;

           public CustomPrincipal(string name, string[] roles, string[] permissions)
           {
               this._roles = roles;
               this._permissions = permissions;
               this._identity = new GenericIdentity(name);
           }

           public IIdentity Identity
           {
               get { return _identity; }
           }

           public bool IsInRole(string role)
           {
               return _roles.Contains(role);
           }

           public bool HasPermission(string permission)
           {
               return _permissions.Contains(permission);
           }
       }

In order for your web service to actually implement the authentication module, you must add it to the modules node of your web.config .

<modules runAllManagedModulesForAllRequests="true">
    <add name="BasicAuthentication" type="UtilitiesWcfService.BasicAuthenticationModule" />
    <remove name="ApplicationInsightsWebTracking" />
    <add name="ApplicationInsightsWebTracking"
    type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule,
    Microsoft.AI.Web" preCondition="managedHandler" />
  </modules>

Store proc Entity with Multiple Result Sets.

I mentioned that my stored procedure aspnet_GetUserCredentials had multiple result sets. In order to achieve this, you must again edit the model that defines the stored procedure via the XML editor and make some changes.

In my case, my stored procedure accepted two input parameters (the application name and the username) and returned 3 result sets:

  1. The first result set contained fields for username , password and passwordsalt .
  2. The second result set contained fields for rolename .
  3. The third result set contained fields for permissionId .

Below are the modifications I needed.

<EntityContainer Name="AuthenticationEntities" 
annotation:LazyLoadingEnabled="true" >
          <FunctionImport Name="aspnet_GetUserCredentials">
            <ReturnType Type="Collection(UtilitiesLightswitchModel.User_SprocResult)" />
            <ReturnType Type="Collection(UtilitiesLightswitchModel.Role_SprocResult)" />
            <ReturnType Type="Collection(UtilitiesLightswitchModel.Permission_SprocResult)" />
            <Parameter Name="ApplicationName" Mode="In" Type="String" />
            <Parameter Name="UserName" 
            Mode="In" Type="String" />
          </FunctionImport>
        </EntityContainer>
        <ComplexType Name="User_SprocResult">
          <Property Type="String" Name="UserName" 
          Nullable="false" MaxLength="256" />
          <Property Type="String" Name="Password" 
          Nullable="false" MaxLength="128" />
          <Property Type="String" Name="PasswordSalt" 
          Nullable="false" MaxLength="128" />
        </ComplexType>
        <ComplexType Name="Role_SprocResult">
          <Property Type="String" Name="RoleName" 
          Nullable="false" MaxLength="256" />
        </ComplexType>
        <ComplexType Name="Permission_SprocResult">
          <Property Type="String" Name="PermissionId" 
          Nullable="false" MaxLength="322" />
        </ComplexType>

...

<FunctionImportMapping FunctionImportName="aspnet_GetUserCredentials" 
 FunctionName="UtilitiesLightswitchModel.Store.aspnet_GetUserCredentials">
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.User_SprocResult">
                <ScalarProperty Name="UserName" ColumnName="UserName" />
                <ScalarProperty Name="Password" ColumnName="Password" />
                <ScalarProperty Name="PasswordSalt" ColumnName="PasswordSalt" />
              </ComplexTypeMapping>
            </ResultMapping>
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.Role_SprocResult">
                <ScalarProperty Name="RoleName" ColumnName="RoleName" />
              </ComplexTypeMapping>
            </ResultMapping>
            <ResultMapping>
              <ComplexTypeMapping TypeName="UtilitiesLightswitchModel.Permission_SprocResult">
                <ScalarProperty Name="PermissionId" ColumnName="PermissionId" />
              </ComplexTypeMapping>
            </ResultMapping>
          </FunctionImportMapping>

Filtering Data

You might want to Filter the entity based on user. Now that we have set the user context via the authentication module, we can now use the user context to filter the result set. You can filter entities by adding a query intercepter. This code below needs to be added to the main service class in my case WcfDataService1.svc . This example shows filtering the Groups entity depending on what role the user belongs to.

/// <summary>
/// Intercept entity query.
/// </summary>
/// <returns>Filtered recordset.</returns>
[QueryInterceptor("Groups")]
public Expression<Func<Group, bool>> OnQueryGroups()
{
    //If this is a group user.
    if (HttpContext.Current.User.IsInRole("GroupUser"))
    {
        //Filter for the specific group id.
        return (Group e) => e.GroupID == HttpContext.Current.User.Identity.Name;
    }
    //If this is a local user.
    else if (HttpContext.Current.User.IsInRole("LocalUser"))
    {
        //Filter for the group containing their site id.
        return (Group e) => e.Sites.Any(r => r.SiteID == HttpContext.Current.User.Identity.Name);
    }
    else
    {
        //Return all.
        return (Group e) => true;
    }
}

You may also want to base your data access rules on the user. To do that, we can add a ChangeInterceptor . In the code below, the user requires the permission CanAddOrEditGroups in order to make changes to the Groups entity.

[ChangeInterceptor("Groups")]
      public void OnChangeGroups(Group group, UpdateOperations operations)
      {
          //Unbox the user principle.
          var u = (BasicAuthenticationProvider.CustomPrincipal)HttpContext.Current.User;

          if (!u.HasPermission("CanAddOrEditGroups"))
          {
              throw new DataServiceException(400, "You do not have permission to add or edit new groups.");
          }
      }

History

  • 2016-03-25: Initial upload




About List